Repository

Матеріал з Вікіпедії — вільної енциклопедії.
Перейти до навігації Перейти до пошуку
UML діаграма, що описує структуру шаблону проєктування Repository

Repository — патерн, який розділяє рівні джерела даних і логіки програми. Часто використовується із патерном Unit Of Work

Переваги та недоліки[ред. | ред. код]

Переваги[ред. | ред. код]

  • Використовується, як колекція
  • Інкапсулює великі запити до БД в методи
  • Рівень абстракції між Business Logic рівнем та Data Access рівнем
  • Ізолює програму від змін джерела даних
  • Джерело даних може бути змінено без будь-яких змін в бізнес логіці і з мінімальними змінами в Репозиторії
  • Полегшує автоматизоване юніт тестування, Test Driven Development
  • Легко створювати mock репозиторію

Недоліки[ред. | ред. код]

  • Зростає кількість класів
  • Погіршує продуктивність
  • Обмежує у використанні особливостей ОРМ фреймворку

Опис мовою C#[ред. | ред. код]

Використаємо Entity Framework. Нехай дано клас-сутність User

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Тепер напишемо інтерфейс репозиторію. Варто зазначити, що репозиторій є колекцією, і має поводитись як колекція, та не містити методів Update(), Save() тощо. Також однією із великих помилок, є те що методи повертають IQueryable замість IEnumerable. Якщо повертати IQueryable це дозволить надбудувати над запитом, ще запити, що не є вірним, оскільки мета цього патерну якраз і є уникнення великих запитів. В такому разі, краще написати ще один метод, який буде виконувати більший запит.

public interface IRepository<TEntity> where TEntity : class
{
    int Count();
    int Count(Expression<Func<TEntity, bool>> predicate);

    IEnumerable<TEntity> Get(Expression<Func<TEntity, bool>> filter = null,
                                Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
                                string includeProperties = "",
                                int? page = null, int? amount = null);
    TEntity Get(int id);

    void Insert(TEntity entity);

    void Delete(object id);
    void Delete(TEntity entityToDelete);
    void Delete(Expression<Func<TEntity, bool>> predicate);
}

Тепер реалізуємо цей інтерфейс у вигляді узагальненого класу. При реалізації ми повертаємо сутність, а не DTO. Мапування — це не відповідальність репозиторію.

public class GenericRepository<TEntity> : IRepository<TEntity> where TEntity : class
{
    // FIELDS
    // узагальнений контекст
    protected DbContext dbContext;
    protected DbSet<TEntity> dbSet;

    // CONSTRUCTORS
    public GenericRepository(DbContext dbContext)
    {
        this.dbContext = dbContext;
        this.dbSet = dbContext.Set<TEntity>();
    }

    // METHODS
    public virtual int Count()
    {
        return dbSet.Count();
    }
    public virtual int Count(Expression<Func<TEntity, bool>> predicate)
    {
        return dbSet.Count(predicate);
    }

    public virtual IEnumerable<TEntity> Get(Expression<Func<TEntity, bool>> filter = null,
                                            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
                                            string includeProperties = "",
                                            int? page = null, int? amount = null)
    {
        // filter
        IQueryable<TEntity> query = dbSet;
        if (filter != null)
        {
            query = query.Where(filter);
        }

        // include properties
        foreach (string includeProperty in includeProperties.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries))
        {
            query = query.Include(includeProperty);
        }

        // ordering
        if (orderBy != null) query = orderBy(query);

        // paging
        if (page.HasValue && amount.HasValue) query = query.Skip((page.Value - 1) * amount.Value).Take(amount.Value);

        return query;
    }
    public virtual TEntity Get(int id)
    {
        return dbSet.Find(id);
    }

    public virtual void Insert(TEntity entity)
    {
        dbSet.Add(entity);
    }

    public virtual void Delete(object id)
    {
        // find
        if (id == null) throw new ArgumentNullException(nameof(id));
        TEntity entityToDelete = dbSet.Find(id);

        // delete finded
        if (entityToDelete == null) throw new InvalidOperationException("There is no records with such id");
        Delete(entityToDelete);
    }
    public virtual void Delete(TEntity entityToDelete)
    {
        if (entityToDelete == null) throw new ArgumentNullException(nameof(entityToDelete));

        if (dbContext.Entry(entityToDelete).State == EntityState.Detached)
        {
            dbSet.Attach(entityToDelete);
        }
        dbSet.Remove(entityToDelete);
    }
    public virtual void Delete(Expression<Func<TEntity, bool>> predicate)
    {
        if (predicate != null) dbSet.RemoveRange(dbSet.Where(predicate));
        else dbSet.RemoveRange(dbSet);
    }
}

Тепер залишилось для кожної сутності реалізувати свій репозиторій. Напишемо інтерфейс, який додаватиме (а можливо і ні) новий функціонал для конкретного репозиторію.

public interface IUserRepository: IRepository<User>
{
    User GetByName(string name);
}

Та конкретна реалізація:

public class UserRepository : GenericRepository<User>, IUserRepository
{
    public UserRepository(DbContext dbContext) 
        : base(dbContext) { }

    public User GetByName(string name)
    {
        return dbSet.First(u => u.Name == name);
    }
}

При потребі варто також узагальнювати тип ключа:

public interface IRepository<TEntity, TKey> where TEntity : class, IEntity<TKey>
{
   . . .
}

Зв'язок з іншими патернами[ред. | ред. код]

  • Unit Of Work та Repository часто використовують в парі.

Реалізація[ред. | ред. код]

Див. також[ред. | ред. код]

Джерела[ред. | ред. код]