Лучшая практика: структура сущностей: где делать объединения, если используются шаблоны репозитория и UoW - PullRequest
0 голосов
/ 13 марта 2019

У меня есть пользовательское хранилище и партнерское хранилище. Мои репозитории не возвращают IQuerables. Пользовательский объект имеет partnerID. Я хочу использовать partnerID, чтобы соединить две таблицы: пользовательскую и партнерскую, используя репозитории с использованием Linq. Однако я не уверен, где делать эти объединения. У партнера и пользователя нет внешнего ключа, поэтому я не могу включить свойство Включить через навигацию.

Я знаю, что объединения не должны идти в хранилище. Должно ли соединение произойти в UoW? Или сервис? Какова лучшая практика с точки зрения того, где я делаю эти объединения?

Ответы [ 2 ]

2 голосов
/ 13 марта 2019

Совокупный корень: https://martinfowler.com/bliki/DDD_Aggregate.html

На ваших агрегатах, который всегда является корнем, будет именем вашего репо, потому что

Любые ссылки извне агрегата должны идти тольков совокупный корень

0 голосов
/ 13 марта 2019

В нашей компании мы отделяем объект, который содержит варианты использования, которые должны быть выполнены (единица работы), от концепции хранения данных (репозиторий) от метода хранения данных (в базе данных, использующей структуру сущностей).

Преимущества отделения DbContext от репозитория

Используя это разделение, можно изменить базу данных на любую другую, в которой хранятся таблицы.Например, вы можете использовать последовательность CSV-файлов или использовать Dapper для доступа к базе данных.

Другое преимущество разделения между Entity Framework и Repository заключается в том, что вы можете предоставлять интерфейсы, которые не предоставляют доступ к элементам.что вы не хотите, чтобы пользователи имели доступ к.Например, некоторые пользователи могут только запрашивать данные, другие могут добавлять или обновлять данные, и лишь немногие могут удалять объекты.

Очень приятный побочный эффект заключается в том, что мы можем модульно протестировать код, который использует хранилище, с помощьюколлекция Test Lists вместо реальных таблиц базы данных.

Только если новые сценарии использования требуют новых данных, нам нужно изменить все три.Пользователи нашей единицы работы, которым не нужны новые данные, не заметят разницу

DbContext

DbSets в DbContext представляют таблицы нашей базы данных.Каждая таблица имеет по крайней мере Id в качестве первичного ключа и обнуляемый объект DateTime, который отмечает дату, когда объект был объявлен устаревшим.Фоновый процесс регулярно удаляет все объекты, которые устарели на некоторое время.

Последняя часть делается для того, чтобы пользователь А не обновлял запись, пока пользователь Б удаляет эту же запись.Пользователи могут только пометить записи как устаревшие, но не могут их удалить.

interface IDbItem
{
    int Id {get; }      // no need to ever change the primary key
    DateTime? Obsolete {get; set;}
}

Например, Клиент:

class Customer : IDbItem
{
     public int Id {get; set;}
     public DateTime? ObsoleteDate {get; set;}

     public string Name {get; set;}
     ... // other properties
}

DbContext поддерживается настолько простым, насколько это возможно: он представляет толькотаблицы и отношения между таблицами

Репозиторий

Репозиторий скрывает, какой метод хранения используется для хранения данных.Это может быть база данных, серия CSV-файлов, данные могут быть разделены на несколько баз данных.

Хранилище обычно имеет несколько интерфейсов:

  • Интерфейс только для запросов, возвращающий IQueryable<...> каждой таблицы, которую вы хотите представить.Пользователи этого интерфейса могут выполнять любые запросы.Они не могут изменить данные.Это имеет то преимущество, что вы можете скрывать свойства и таблицы, которые вы не хотите показывать.Пользователи не могут случайно изменить элементы.
  • Интерфейс для создания / обновления элементов, а также запроса.Для немногих форм, которые действительно добавляют или обновляют базу данных.Они также могут помечать элементы как устаревшие.
  • Интерфейс для удаления данных, помеченных Obsolete.Используется фоновым процессом для регулярного удаления устаревших данных.

Точно так же, как структура сущностей имеет классы, которые представляют сущности (таблицы: клиенты, заказы, строки заказа, ...) и классы, представляющие коллекцию.объектов (IDbSet<Customer>), хранилище имеет похожие классы и интерфейсы.Большинство из них можно использовать повторно, а однострочные

классы сущностей репозитория

interface IId
{
    int Id {get;}
}

interface IRepositoryEntity : IId
{
    bool IsObsolete {get;}
    void MarkObsolete();
}

Каждый элемент репозитория может быть помечен как устаревший.Общий базовый класс:

class RepositoryEntity<TSource> : IId, IRepositoryEntity
      where TSource : IDbItem
{
     public TSource DbItem {get; set;}

     // Interface IId
     public int Id => this.DbItem.Id;

     // Interface IRepositoryEntity
     public bool IsObsolete => this.DbItem.ObsoleteDate != null;
     public void MarkObsolete()
     {
         this.DbItem.ObsoleteDate = DateTime.UtcNow;
     }
}

Например, Клиент:

interface IReadOnlyCustomer : IId
{
    string Name {get;}
    ...
}
interface ICustomer : IRepositoryItem
{
    string Name {get; set;}
}
class Customer : RepositoryEntity<Customer>, IReadOnlyCustomer, ICustomer
{
     // Interfaces IId and IRepositoryItem implemented by base class

     // Interface ICustomer
     public string Name {get; set;}
     ...

     // Interface IReadOnlyCustomer
     string IReadOnlyCustomer.Name => this.Name;
     ...
}

Вы видите, что Клиенту Репозитария нужно только реализовать свойства Клиента, которые вы действительно хотите предоставить внешнимМир.Хранилище не должно представлять ваши таблицы базы данных.

Например, если ваша база данных имеет разделенные значения для Customer FirstName, MiddleName, FamilyName, то вы можете объединить их в get Namefunction.

Репозиторий коллекций

Репозиторий коллекций похож на IDbSet<...>.Есть интерфейс к Query only, а один к Query, Update, Mark Obsolete.Конечно, у нас также есть полный доступ, предоставленный счастливым немногим.

Для ReadOnly достаточно иметь IQueryable<TEntity> where TEntity : Iid

Для запроса / добавления / обновления / устаревшего мне нужен ISet иНабор:

interface ISet<TEntity> : IQueryable<TEntity> where TEntity: IRepositoryEntity
{
     TEntity Add(TEntity item);
}

class Set<TEntity, TDbEntity> : ISet<TEntity>
  where TEntity: IRepositoryEntity,
  where TDbEntity: IDbItem
{
     public IDbSet<TEntity> DbSet {get; set;}

     // implement the interfaces via DbSet
     public TEntity Add(TEntity item)
     {
         // TODO: convert item to a dbItem
         return this.DbSet.Add(dbItem);
     }
     // Similar for IQueryable<TEntity> and IQueryable
}

Интерфейс для доступа ReadOnly и для доступа CRUD:

interface IReadOnlyRepository : IDisposable
{
     IQueryable<IReadOnlyCustomer> Customers {get;}
     IQueryable<IReadOnlyOrders> Orders {get;}
}
interface IRepository : IDisposable
{
     ISet<ICustomer> Customers {get;}
     ISet<IOrder> Orders {get;}
     void SaveChanges();
}

Те, кто имеет доступ к ReadOnlyRepository, могут запрашивать только данные. Они не могут вносить какие-либо изменения. Те, кто имеет доступ к IRepository, могут добавлять элементы, обновлять элементы и сохранять изменения.

Репозиторий классов реализует все интерфейсы:

class Repository : IReadOnlyRepository,     // Query Only
 IRepository,                               // Query, Add and Update
 IDisposable
{
     private readonly dbContext = new CustomerDbContext();
     // TODO: Dispose() will Dispose dbContext

     // Used by the other interfaces
     protected IDbSet<Customer> Customers => this.dbContext.Customers;
     protected IDbSet<Orders> Orders => this.dbContext.Orders;
     void SaveChanges() {this.dbContext.SaveChanges();}

     // IRepository:
     ISet<ICustomer> IRepository.Customers => new Set<Customer>{DbSet = this.Customers};
     ISet<IOrder> IRepository.Orders => new Set<Order>{DbSet = this.Orders};
     void IRepository.SaveChanges() {this.DbContext.SaveChanges();}

     // IReadOnlyRepository
     IQueryable<IReadOnlyCustomer> IReadOnlyRepository.Customers => this.Customers;
     IQueryable<IReadOnlyOrders> IReadOnlyRepository.Orders => this.Orders;
}

Кажется, что кода много, но большинство функций - это однострочные, которые вызывают соответствующую функцию Entity Framework.

Наконец, нам нужна фабрика, которая создает хранилище. Если вы хотите повторно использовать это для нескольких репозиториев, создайте универсальный фабричный класс. Для простоты я создаю его для базы данных заказов:

class OrdersRepository
{
    public IReadOnlyRepository CreateReadOnly()
    {
         // TODO: if desired check rights: can this user access this database?
         return new Repository();
    }
    public IRepository CreateUpdateAccess()
    {
         // TODO: if desired check rights: can this user access this database?
         return new Repository();
    }
    public Repository CreateFullControl()
    {
         // TODO: if desired check rights: can this user access this database?
         return new Repository();
    }

Фактически: для фонового процесса, который удаляет все устаревшие элементы, у нас есть специальный интерфейс, который удаляет все элементы, которые устарели на некоторое время. Здесь больше не упоминается.

Использование:

var repositoryFactory = new RepositoryFactory() {AccessRights = ...}

// I need to query only:
using (var repository = repositoryFactory.CreateUpdatAccess())
{
     // you can query, change value and save changes, for instance after a Brexit:
     var customersToRemove = repository.Customers.Where(customer => customer.State == "United Kingdom")
     foreach (var customerToRemove in customersToRemove);
     {
         customerToRemove.MarkObsolete();
     }
     repository.SaveChanges();
}

// I need to change data:
using (var repository = repositoryFactory.CreateReadOnly())
{
     // do some queries. Compiler error if you try to change
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...