Страницы

Поиск по вопросам

понедельник, 23 декабря 2019 г.

Как правильно организовать работу с данными (C# Desktop App, Database)?

#c_sharp #sqlite #desktop


Упрощенная схема БД


Нужна помощь в организации работы с данными.
Сабжект: десктопное приложение для гостиницы. 
В таблице Reservations данные по каждому размещению. Каждое размещение может состоять
из одного или нескольких дней (ResDays). На каждый день размещения может быть добавлена
доп. услуга (AddServices).

В программе для этого я использую коллекции. Очень упрощенно:

class AddServiceItem
{
   string Name;
   Decimal Cost;
}
class ResDayItem
{
   DateTime Date;
   Decimal Cost;
   Collection AddServices;
}
class Reservation
{
   DateTime StartDate;
   DateTime StartDate;
   int Status;
   Collection ResDays;
}


С созданием размещения проблем нет - пользователь выбирает количество дней, создается
экземпляр Reservation, а коллекции из дней и доп. услуг заполняются юзером в интерфейсе
программы. После нажатия кнопки "Добавить размещение" все записывается в БД.

Конечно, нужно иметь возможность изменять все данные. При открытии размещения для
редактирования, я опять создаю экземпляр Reservation, а коллекции заполняю значениями
из БД примерно так

MyReservation = new Reservation();
//создаю коллекцию дней
MyReservation.ResDays = new Collection();
//и для каждого дня размещения с Id == resId
foreach (var Day in db.Table().Where(x => x.ResId == resId))
{   //создаю экземпляр ResDayItem
    ResDayItem DayItem = new ReservationDayItem();
  //и в нем коллекцию доп. услуг
  DayItem.AddServices = new Collection();
    foreach (var AddService in db.Table().Where(x => x.ResDayId == Day.Id))
        DayItem.AddServices.Add(AddService);
    MyReservation.ResDays.Add(DayItem);
}


Вопрос в следующем: как (в каком порядке) делать апдейт БД при изменении коллекций?
Хотелось бы, чтобы в БД все писалось при нажатии пользователем кнопки "Сохранить".
Если количество дней/доп.услуг осталось прежним, то это просто update по всем записям,
а если юзер удалил что-то? Как узнать, какие именно элементы коллекций были удалены,
чтобы удалить соответствующие записи из БД?
Может быть я вообще применил неверный подход с такой вложенностью коллекций?
Как правильно организовать работу с данными в такой задаче?
    


Ответы

Ответ 1



В более-менее сложных программах реализуют примерно такую структуру Как пример реализации паттернов Repository и Unit of Work могу предложить вам следующий вариант. Для Repository мы будем опираться на следующую иллюстрацию Для Unit of Work мы будем опираться на следующую иллюстрацию 1) Создайте проект Class Library LibDomain, создайте в нем две папки: Repositories и Models. 2) Cоздайте след. классы в папке Models public class Reservation { //ctor public Reservation() => Days = new HashSet(); public long Id { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } public int Status { get; set; } public virtual ICollection Days { get; set; } } `` public class ReservedDay { //ctor public ReservedDay() => Services = new HashSet(); public long Id { get; set; } public DateTime Date { get; set; } public decimal Cost { get; set; } //внешний ключ public long ReservationId { get; set; } //навигационное св-во public virtual Reservation Reservation { get; set; } public ICollection Services { get; set; } } `` public class AdditionService { public int Id { get; set; } public string Name { get; set; } public decimal Cost { get; set; } //внешний ключ public long ReservedDayId { get; set; } //навигационное св-во public virtual ReservedDay Day { get; set; } } `` 3) в папке репозиториев вам нужно будет создать главный интерфейс репозитория public interface IRepository where TEntity : class { //Queries Task> Get(); Task Get(long id); Task> Find(Expression> predicate); //Commands Task Add(TEntity entity); Task AddRange(IEnumerable entities); void Remove(TEntity entity); void RemoveRange(IEnumerable entities); } и на каждую сущность свой интерфейса репозитория (даю пример только двух) public interface IReservationRepository : IRepository { //здесь можно добавить объявление необходимых методов //актуальных для данной сущности Task GetByIdWithDays(long id); Task GetByIdWithDaysAndServices(long id); } `` public interface IReservedDayRepository : IRepository { //здесь можно добавить объявление необходимых методов //актуальных только для данной сущности } создайте в корне проекта интерфейс для UnitOfWork public interface IUnitOfWork : IDisposable { IReservationRepository Reservations { get; } IReservedDayRepository ReservedDays { get; } IAdditionServiceRepository AdditionServices { get; } Task Commit(); } 4) Создайте в решении еще один проект библиотеки LibData, в котором мы непосредственно реализуем работу с SQLite через Entity Framework Core. Добавьте ссылку на LibDomain и установите через NuGet Package Manager пакет Microsoft.EntityFrameworkCore.Sqlite (важно! устанавливайте версию 1.1.2, а не самую последнюю 2.0, которая вышла несколько дней назад, иначе будут проблемы). 5) в проекте создайте класс SqliteDbContext public class SqliteDbContext : DbContext { //ctor public SqliteDbContext(DbContextOptions options) : base(options) { //создание БД, если она не была ранее создана Database.EnsureCreated(); } public virtual DbSet Reservations { get; set; } public virtual DbSet ReservedDays { get; set; } public virtual DbSet AdditionServices { get; set; } } и создайте папку Repositories и в ней класс репозитория public abstract class Repository : IRepository where TEntity : class { protected readonly DbContext _context; //ctor public Repository(DbContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } //Queries public virtual async Task> Get() { return await _context.Set().ToArrayAsync(); } public virtual async Task Get(long id) { return await _context.Set().FindAsync(id); } public virtual async Task> Find(Expression> predicate) { return await _context.Set().Where(predicate).ToArrayAsync(); } //Commands public virtual Task Add(TEntity entity) { return _context.AddAsync(entity); } public virtual Task AddRange(IEnumerable entities) { return _context.AddRangeAsync(entities); } public virtual void Remove(TEntity entity) { _context.Remove(entity); } public virtual void RemoveRange(IEnumerable entities) { _context.RemoveRange(entities); } } и классы для каждой сущности: ReservationRepository, ReservedDayRepository, AdditionServiceRepository (пример только для первого класса) public class ReservationRepository : Repository, IReservationRepository { private readonly SqliteDbContext _context; //ctor public ReservationRepository(SqliteDbContext context) : base(context) { _context = context; } //далее реализация методов из IReservationRepository //или переопределение методов из Repository под Reservation public Task GetByIdWithDays(long id) { return _context.Reservations .Include(r => r.Days) .FirstAsync(r => r.Id == id); } public Task GetByIdWithDaysAndServices(long id) { return _context.Reservations .Include(r => r.Days) .ThenInclude(d => d.Services) .FirstAsync(r => r.Id == id); } } 6) создайте класс, кот. реализутет интерфейс IUnitOfWork public class SqliteDataService : IUnitOfWork { private readonly SqliteDbContext _context; //ctor public SqliteDataService(SqliteDbContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); Reservations = new ReservationRepository(_context); ReservedDays = new ReservedDayRepository(_context); AdditionServices = new AdditionServiceRepository(_context); } public IReservationRepository Reservations { get; private set; } public IReservedDayRepository ReservedDays { get; private set; } public IAdditionServiceRepository AdditionServices { get; private set; } public Task Commit() => _context.SaveChangesAsync(); public void Dispose() => _context.Dispose(); /// /// Соединение с БД и передача экземпляра этого сервиса /// /// строка соединения с БД /// экземпляр этого сервиса public static SqliteDataService GetService(string connectionString) { if (String.IsNullOrEmpty(connectionString)) throw new ArgumentNullException(nameof(connectionString)); var dbBuilder = new DbContextOptionsBuilder(); dbBuilder.UseSqlite(connectionString); return new SqliteDataService(new SqliteDbContext(dbBuilder.Options)); } } 7) Пересмотрите еще раз иллюстрации, перечитайте код примеров. Надеюсь вам станет яснее схема взаимодействия с БД. В качестве примера использования привожу пример простого консольного приложения class Program { static void Main(string[] args) { string dbPath = @"Filename=D:\repExample.sqlite"; var dbService = SqliteDataService.GetService(dbPath); //AddReservation(dbService); //AddServicesToReservation(dbService); ShowReservation(dbService); Console.ReadKey(); } private static async void ShowReservation(SqliteDataService dbService) { //находим размещение c его днями и услугами по его id var res = await dbService.Reservations.GetByIdWithDaysAndServices(1); Console.WriteLine(); Console.WriteLine($"Размещение с {res.StartDate.ToShortDateString()} по {res.EndDate.ToShortDateString()}"); Console.WriteLine(new string('=', 70)); int dayNumber = 1; int servNumber = 1; foreach (var day in res.Days) { Console.WriteLine(); Console.WriteLine($"День {dayNumber} - {day.Date.ToShortDateString()}"); foreach (var service in day.Services) { Console.WriteLine(); Console.WriteLine($"Услуга {servNumber}:{service.Name} за {service.Cost}руб."); servNumber++; } Console.WriteLine(new string('-', 30)); dayNumber++; servNumber = 1; } } private static async void AddReservation(SqliteDataService dbService) { //создаем дни размещения ReservedDay day1 = new ReservedDay() { Date = new DateTime(year: 2017, month: 8, day: 20), Cost = 200M }; ReservedDay day2 = new ReservedDay() { Date = new DateTime(year: 2017, month: 8, day: 21), Cost = 200M }; ReservedDay day3 = new ReservedDay() { Date = new DateTime(year: 2017, month: 8, day: 22), Cost = 200M }; //размещение и его дни Reservation res = new Reservation() { StartDate = day1.Date, EndDate = day3.Date, Status = 1 }; res.Days.Add(day1); res.Days.Add(day2); res.Days.Add(day3); //добавляем и сохраняем в БД await dbService.Reservations.Add(res); await dbService.Commit(); } private static async void AddServicesToReservation(SqliteDataService dbService) { var serv1 = new AdditionService { Name = "Бассейн", Cost = 2M }; var serv2 = new AdditionService { Name = "Парилка", Cost = 1M }; var serv3 = new AdditionService { Name = "Завтрак в номер", Cost = 100M }; var serv4 = new AdditionService { Name = "Чай в номер", Cost = 10M }; //находим размещение по его id var res = await dbService.Reservations.GetByIdWithDays(1); //выбираем дни по дате var dateDay1 = new DateTime(year: 2017, month: 8, day: 20); var dateDay2 = new DateTime(year: 2017, month: 8, day: 21); var dateDay3 = new DateTime(year: 2017, month: 8, day: 22); var day1 = res.Days.Single(d => d.Date == dateDay1); var day2 = res.Days.Single(d => d.Date == dateDay2); var day3 = res.Days.Single(d => d.Date == dateDay3); //добавляем услуги в эти дни day1.Services.Add(serv1); day1.Services.Add(serv4); day2.Services.Add(serv3); day2.Services.Add(serv1); day2.Services.Add(serv2); day3.Services.Add(serv4); //запоминаем await dbService.Commit(); } } Целиком пример можно скачать здесь.

Комментариев нет:

Отправить комментарий