Создание гистограммы еженедельных данных из SQLite.NET

Я использую Xamarin.Forms, Microcharts и SQLite.NET для создания мобильного приложения. В базе данных SQLite.NET хранятся сведения о книгах (идентификатор книги и дата входа – дата, когда она была введена в систему).

На гистограмме отображается количество книг, введенных на этой неделе каждый день – с понедельника по воскресенье.

Однако такая реализация кажется неэффективной. Кроме того, поскольку поля DateTime в базе данных не имеют эквивалента свойству DateTime.Date в .NET, запрос проверяет период между двумя датами, чтобы получить счетчик за каждый день.

using SkiaSharp;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using Microcharts;
using ChartEntry = Microcharts.ChartEntry;

public GraphDisplayPage()
{
     InitializeComponent();
     DrawChart();
}

void DrawChart()
{
     var monday = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + (int)DayOfWeek.Monday);
     int mondayBookCount = App.Database.GetDailyCount(monday);

     var tuesday = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + (int)DayOfWeek.Tuesday);
     int tuesdayBookCount = App.Database.GetDailyCount(tuesday);

     var wednesday = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + (int)DayOfWeek.Wednesday);
     int wednesdayBookCount = App.Database.GetDailyCount(wednesday);

     var thursday = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + (int)DayOfWeek.Thursday);
     int thursdayBookCount = App.Database.GetDailyCount(thursday);

     var friday = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + (int)DayOfWeek.Friday);
     int fridayBookCount = App.Database.GetDailyCount(friday);

     var saturday = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + (int)DayOfWeek.Saturday);
     int saturdayBookCount = App.Database.GetDailyCount(saturday);

     var sunday = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + (int)DayOfWeek.Sunday);
     int sundayBookCount = App.Database.GetDailyCount(sunday);

     List<ChartEntry> entries = new List<ChartEntry>
     {
          new ChartEntry(mondayBookCount)
          {
               Label = "Monday",
               ValueLabel = mondayBookCount.ToString(),
               Color = SKColor.Parse("#004daa")
           },
           new ChartEntry(tuesdayBookCount)
           {
               Label = "Tuesday",
               ValueLabel = tuesdayBookCount.ToString(),
               Color = SKColor.Parse("#004daa")
           },
           new ChartEntry(wednesdayBookCount)
           {
                Label = "Wednesday",
                ValueLabel = wednesdayBookCount.ToString(),
                Color = SKColor.Parse("#004daa")
            },
            new ChartEntry(thursdayBookCount)
            {
                Label = "Thursday",
                ValueLabel = thursdayBookCount.ToString(),
                Color = SKColor.Parse("#004daa")
            },
            new ChartEntry(fridayBookCount)
            {
                 Label = "Friday",
                 ValueLabel = fridayBookCount.ToString(),
                 Color = SKColor.Parse("#004daa")
            },
            new ChartEntry(saturdayBookCount)
            {
                 Label = "Saturday",
                 ValueLabel = saturdayBookCount.ToString(),
                 Color = SKColor.Parse("#004daa")
            },
            new ChartEntry(sundayBookCount)
            {
                 Label = "Sunday",
                 ValueLabel = sundayBookCount.ToString(),
                 Color = SKColor.Parse("#004daa")
            }
         };

       chartView.Chart = new BarChart { Entries = entries, LabelTextSize = 32f, LabelOrientation = Orientation.Horizontal, ValueLabelOrientation = Orientation.Horizontal, Margin = 20 };
}

Book.cs

   public class Book : INotifyPropertyChanged
   {
        [PrimaryKey, AutoIncrement]
        public int ID { get; set; }
        private DateTime bookSaveTime;
        public DateTime BookSaveTime
        {
            get
            {
                 return bookSaveTime;
             }
             set
             {
                 if (bookSaveTime != value)
                 {
                     bookSaveTime= value;
                     OnPropertyChanged("BookSaveTime");
                 }
              }
         }
    
         public event PropertyChangedEventHandler PropertyChanged;
         protected virtual void OnPropertyChanged(string propertyName)
         {
              var changed = PropertyChanged;
              if (changed != null)
              {
                  PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
              }
         }
   }

SQLiteDatabase.cs:

static SQLiteConnection database;
public const string DbFileName = "SQLite.db3";
public string CurrentState;

public SQLiteDatabase()
{
            try
            {
                database = DependencyService.Get<ISQLiteService>().GetConnection(DbFileName);
                database.CreateTable<Book>();

                CurrentState = "Database created";

            }
            catch (SQLiteException ex)
            {
                CurrentState = ex.Message;
            }
}

public int GetDailyCount(DateTime day)
{
      var dayAfterCurrentDay = day.AddDays(1); 
      return database.ExecuteScalar<int>("SELECT COUNT(*) FROM Book WHERE bookSaveTime> ? AND bookSaveTime< ?;", day, dayAfterCurrentDay);   
}

Можно ли это улучшить?

1 ответ
1

В этом обзоре позвольте мне сосредоточиться на одном моменте: устранение дубликатов.

В чем проблема с дубликатами?

  1. Размер вашей кодовой базы может стать довольно большим. Это портит читаемость.
  2. Если вам нужно изменить одну вещь в функциональности, которая влияет на все дубликаты, вы должны сделать это во всех случаях.
  3. Если вам нужно расширить свой источник данных, рекомендуется использовать копипаст, который подвержен ошибкам. С помощью Ctrl + C и Ctrl + V вы можете забыть изменить важное свойство / вызов метода / все, что дублируется.

Как я могу это устранить?

  • Краткий ответ: Ослабляя связь между данными и функциональностью.
  • В этом конкретном случае ваша реализация снова и снова выполняет одни и те же функции для разных данных.
  • Итак, сначала вы должны определить, какая часть функциональности выполняется несколько раз:
    • Здесь я использовал {xyz} для обозначения заполнителей, которые меняются для каждого дубликата.
var {dayOfWeekDate} = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + (int){dayOfWeekEnumCalue});
int {dayOfWeekBookCount} = App.Database.GetDailyCount({dayOfWeekDate});

{dayOfWeekEnumValue}

  • Вы можете получить все значения DayOfWeek используя Enum.GetValues функциональность.
var days = (DayOfWeek[]) Enum.GetValues(typeof(DayOfWeek));
  • Мы можем перебирать эту коллекцию, чтобы каждый день получать:
foreach (var day in days)
{
    var {dayOfWeekDate} = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + (int)day);
    var {dayOfWeekBookCount} = App.Database.GetDailyCount({dayOfWeekDate});
}

{dayOfWeekDate}

  • Эта переменная предназначена для foreach. Итак, здесь мы можем использовать любое имя, какое захотим.
    • Мы можем назвать его, описав, какие данные на нем хранятся.
    • Это лучше, чем называть его тем, как мы хотим его использовать. Если нам нужно расширить функциональность и использовать эту переменную в нескольких вызовах методов, тогда именование может стать проблематичным.
foreach (var day in days)
{
    var specificDayAtMidnight = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + (int)day);
    var {dayOfWeekBookCount} = App.Database.GetDailyCount(specificDayAtMidnight);
}

{dayOfWeekBookCount}

  • Здесь мы расширяем круг наших интересов.
    • Где и как мы хотим использовать эти данные?
new ChartEntry({dayOfWeekBookCount})
{
   Label = "Monday",
   ValueLabel = {dayOfWeekBookCount}.ToString(),
   Color = SKColor.Parse("#004daa")
},
  • Как мы видим, это конкретный день. Итак, мы должны связать день и количество книг.
    • Это легко сделать, например, используя Dictionary:
Dictionary<string, int> dailyBookCounts =  new Dictionary<string, int>();
  • Здесь key это название дня и value это количество книг.
  • Мы должны заполнить эту коллекцию внутри нашего foreach петля:
foreach (var day in days)
{
    var specificDayAtMidnight = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + (int)day);
    var bookCount = App.Database.GetDailyCount(specificDayAtMidnight);
    dailyBookCounts.Add(day.ToString("G"), bookCount);
}

entries

  • Имея эти данные в наших руках, мы можем устранить дубликаты ChartEntry создание экземпляра.
List<ChartEntry> entries = new List<ChartEntry>();
foreach (var dailyBookCount in dailyBookCounts)
{
    entries.Add(new ChartEntry(dailyBookCount.Value)
    {
        Label = dailyBookCount.Key,
        ValueLabel = dailyBookCount.Value.ToString(),
        Color = SKColor.Parse("#004daa")
    });
}
foreach (var (nameOfTheDay, bookCountOfTheDay) in dailyBookCounts)
{
    entries.Add(new ChartEntry(bookCountOfTheDay)
    {
        Label = nameOfTheDay,
        ValueLabel = bookCountOfTheDay.ToString(),
        Color = SKColor.Parse("#004daa")
    });
}

Как мой код выглядит после этих изменений?

//Extract
var days = (DayOfWeek[]) Enum.GetValues(typeof(DayOfWeek));
var dailyBookCounts =  new Dictionary<string, int>();
foreach (var day in days)
{
    var specificDayAtMidnight = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + (int)day);
    var bookCount = App.Database.GetDailyCount(specificDayAtMidnight);
    dailyBookCounts.Add(day.ToString("G"), bookCount);
}

//Transform
var entries = new List<ChartEntry>();
foreach (var (nameOfTheDay, bookCountOfTheDay) in dailyBookCounts)
{
    entries.Add(new ChartEntry(bookCountOfTheDay)
    {
        Label = nameOfTheDay,
        ValueLabel = bookCountOfTheDay.ToString(),
        Color = SKColor.Parse("#004daa")
    });
}

//Load
chartView.Chart = new BarChart
{
    Entries = entries, 
    LabelTextSize = 32f, 
    LabelOrientation = Orientation.Horizontal, 
    ValueLabelOrientation = Orientation.Horizontal, 
    Margin = 20
};
  • Как видите, я добавил 3 строки комментария. Я использовал здесь Извлечь, преобразовать и загрузить (или, короче, ETL) концепция для разделения фаз вашего метода. Это улучшает читаемость, помогая сопровождающему вашего кода, где найти определенную часть функциональности.

Есть ли что-то еще, что можно было бы улучшить?

  • Да, есть две основные проблемы, которые можно решить:
    • Асинхронный ввод-вывод
    • Пакетная операция

Асинхронный ввод-вывод

  • Всякий раз, когда вы выполняете запрос к базе данных, вызывая App.Database.GetDailyCount вы блокируете выполнение своего кода до тех пор, пока не завершится извлечение данных. Что неплохо, но твой Thread ничего не делает, просто ждет ответа. Таким образом, вы в основном тратите ценные вычислительные ресурсы.
  • Если вы будете использовать асинхронные (неблокирующие) вызовы ввода-вывода, тогда, пока база данных выполняет запрос, ваш Thread будет освобожден и сможет выполнять любой другой код. Таким образом, это может улучшить пропускную способность вашей системы (позволяя выполнять вычисления на основе ЦП, пока приложение ожидает завершения операции ввода-вывода).

Пакетная операция

  • В вашей текущей реализации вы выполняете 7 отдельных вызовов базы данных. Это означает, что между вашим приложением и базой данных существует 7 циклов туда и обратно. Это не проблема на вашей машине разработчика, потому что, скорее всего, оба работают на одной машине. Но в случае производственной среды они, вероятно, разделены и работают на разных машинах. Это означает, что существует задержка в сети, которую также следует учитывать.
  • Разрешив выполнять пакетную операцию на стороне базы данных, вы можете уменьшить количество циклов приема-передачи до 1, что означает, что задержка в сети может стать незначительной.

Ваш код будет выглядеть примерно так:

var startDates = new List<DateTime>();
foreach (var day in days)
{
    startDates.Add(DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + (int)day));
}

Dictionary<string, int> dailyBookCounts = await App.Database.GetWeeklyCountAsync(startDates);

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *