각 객체에 대해 일반 리포지토리와 특정 리포지토리를 만들면 어떤 이점이 있습니까?


132

ASP.NET MVC 응용 프로그램을 개발 중이며 이제 리포지토리 / 서비스 클래스를 구축하고 있습니다. 모든 리포지토리가 구현하는 일반 IRepository 인터페이스를 만드는 것과 각 리포지토리에 고유 한 인터페이스와 메서드 집합이있는 주요 이점이 있는지 궁금합니다.

예를 들어, 일반적인 IRepository 인터페이스는 다음과 같습니다 ( 이 답변 에서 가져옴 ).

public interface IRepository : IDisposable
{
    T[] GetAll<T>();
    T[] GetAll<T>(Expression<Func<T, bool>> filter);
    T GetSingle<T>(Expression<Func<T, bool>> filter);
    T GetSingle<T>(Expression<Func<T, bool>> filter, List<Expression<Func<T, object>>> subSelectors);
    void Delete<T>(T entity);
    void Add<T>(T entity);
    int SaveChanges();
    DbTransaction BeginTransaction();
}

각 리포지토리는이 인터페이스를 구현합니다 (예 :

  • 고객 리포지토리 :이 리포지토리
  • 제품 리포지토리 :이 리포지토리
  • 기타

이전 프로젝트에서 수행 한 대안은 다음과 같습니다.

public interface IInvoiceRepository : IDisposable
{
    EntityCollection<InvoiceEntity> GetAllInvoices(int accountId);
    EntityCollection<InvoiceEntity> GetAllInvoices(DateTime theDate);
    InvoiceEntity GetSingleInvoice(int id, bool doFetchRelated);
    InvoiceEntity GetSingleInvoice(DateTime invoiceDate, int accountId); //unique
    InvoiceEntity CreateInvoice();
    InvoiceLineEntity CreateInvoiceLine();
    void SaveChanges(InvoiceEntity); //handles inserts or updates
    void DeleteInvoice(InvoiceEntity);
    void DeleteInvoiceLine(InvoiceLineEntity);
}

두 번째 경우, 표현식 (LINQ 또는 기타)은 리포지토리 구현에 완전히 포함되며 서비스를 구현하는 사람은 호출 할 리포지토리 함수를 알아야합니다.

서비스 클래스에서 모든 표현식 구문을 작성하고 리포지토리로 전달하는 이점을 보지 못합니다. 이것은 messup하기 쉬운 LINQ 코드가 많은 경우에 복제되고 있다는 것을 의미하지 않습니까?

예를 들어 기존 인보이스 발행 시스템에서는

InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)

몇 가지 다른 서비스 (고객, 송장, 계정 등)에서 여러 곳에서 다음을 작성하는 것보다 훨씬 깔끔해 보입니다.

rep.GetSingle(x => x.AccountId = someId && x.InvoiceDate = someDate.Date);

특정 접근 방식을 사용할 때의 유일한 단점은 Get * 함수의 많은 순열로 끝날 수 있다는 것입니다. 그러나 여전히 식 논리를 서비스 클래스로 푸시하는 것이 바람직합니다.

내가 무엇을 놓치고 있습니까?


전체 ORM과 함께 일반 리포지토리를 사용하면 쓸모가 없습니다. 이에 대해서는 여기 에서 자세히 논의했습니다 .
Joshi를 Amit

답변:


169

이것은 리포지토리 패턴만큼 오래된 문제입니다. IQueryable쿼리의 균일 한 표현 인 LINQ가 최근 소개되면서이 주제에 대해 많은 논의가있었습니다.

나는 일반적인 저장소 프레임 워크를 구축하기 위해 열심히 노력한 후에 특정 저장소를 선호합니다. 어떤 영리한 메커니즘을 시도하더라도 항상 같은 문제가 발생했습니다. 리포지토리는 모델링되는 도메인의 일부이며 해당 도메인은 일반적인 것이 아닙니다. 모든 엔터티를 삭제할 수있는 것은 아니며 모든 엔터티를 추가 할 수있는 것은 아니며 모든 엔터티에 리포지토리가있는 것은 아닙니다. 쿼리는 매우 다양합니다. 저장소 API는 엔티티 자체만큼 고유합니다.

내가 자주 사용하는 패턴은 특정 저장소 인터페이스를 갖지만 구현을위한 기본 클래스를 갖는 것입니다. 예를 들어 LINQ to SQL을 사용하면 다음을 수행 할 수 있습니다.

public abstract class Repository<TEntity>
{
    private DataContext _dataContext;

    protected Repository(DataContext dataContext)
    {
        _dataContext = dataContext;
    }

    protected IQueryable<TEntity> Query
    {
        get { return _dataContext.GetTable<TEntity>(); }
    }

    protected void InsertOnCommit(TEntity entity)
    {
        _dataContext.GetTable<TEntity>().InsertOnCommit(entity);
    }

    protected void DeleteOnCommit(TEntity entity)
    {
        _dataContext.GetTable<TEntity>().DeleteOnCommit(entity);
    }
}

DataContext선택한 작업 단위로 교체하십시오 . 구현 예는 다음과 같습니다.

public interface IUserRepository
{
    User GetById(int id);

    IQueryable<User> GetLockedOutUsers();

    void Insert(User user);
}

public class UserRepository : Repository<User>, IUserRepository
{
    public UserRepository(DataContext dataContext) : base(dataContext)
    {}

    public User GetById(int id)
    {
        return Query.Where(user => user.Id == id).SingleOrDefault();
    }

    public IQueryable<User> GetLockedOutUsers()
    {
        return Query.Where(user => user.IsLockedOut);
    }

    public void Insert(User user)
    {
        InsertOnCommit(user);
    }
}

리포지토리의 공개 API를 사용하면 사용자를 삭제할 수 없습니다. 또한 노출 IQueryable은 다른 전체 웜 캔일 수 있습니다. 해당 주제에 대한 배꼽과 같은 많은 의견이 있습니다.


9
진심으로 좋은 대답입니다. 감사!
삐 삐 소리 :

5
그렇다면 어떻게 IoC / DI를 사용 하시겠습니까? (IoC의 초보자입니다.) 귀하의 패턴과 관련하여 제 질문은 stackoverflow.com/questions/4312388/…
dan

36
"리포지토리는 모델링되는 도메인의 일부이며 해당 도메인은 일반적인 것이 아닙니다. 모든 엔터티를 삭제할 수있는 것은 아니며 모든 엔터티를 추가 할 수있는 것은 아니며 모든 엔터티에 리포지토리가있는 것은 아닙니다."
adamwtiko

나는 이것이 오래된 대답이라는 것을 알고 있지만 의도적으로 Repository 클래스에서 Update 메소드를 제외하면 궁금합니다. 이 작업을 수행하는 깨끗한 방법을 찾는 데 문제가 있습니다.
rtf

1
Oldie이지만 goodie. 이 글은 믿을 수 없을만큼 현명하며 읽고 잘 읽어야하지만 모든 개발자가해야 할 일입니다. 감사합니다 @BryanWatts. 내 구현은 일반적으로 dapper 기반이지만 전제는 동일합니다. 기능을 선택하는 도메인을 나타내는 특정 리포지토리가있는 기본 리포지토리
pimbrouwers

27

나는 실제로 Bryan의 게시물에 약간 동의하지 않습니다. 나는 그가 옳다고 생각합니다. 궁극적으로 모든 것이 매우 독창적입니다. 그러나 동시에 디자인 할 때 대부분이 나오고 모델을 개발하는 동안 일반 리포지토리를 가져 와서 사용하면 앱을 매우 빠르게 얻을 수 있습니다. 그렇게해야합니다.

따라서 그러한 경우에는 종종 전체 CRUD 스택이있는 일반 IRepository를 만들었으며 API를 사용하여 신속하게 UI를 사용하고 사람들이 UI를 사용하여 통합 및 사용자 승인 테스트를 수행 할 수있게했습니다. 그런 다음 리포지토리에 대한 특정 쿼리가 필요하다는 것을 알았을 때 필요한 경우 특정 종속성으로 해당 종속성을 교체하고 거기서 시작합니다. 하나의 기본 impl. 쉽게 만들고 사용할 수 있습니다 (그리고 메모리 내 db 또는 정적 객체 또는 조롱 된 객체 또는 기타 것에 연결 가능).

즉, 내가 최근에 시작한 것은 행동을 깨뜨리는 것입니다. 따라서 IDataFetcher, IDataUpdater, IDataInserter 및 IDataDeleter에 대한 인터페이스를 수행하는 경우 (예를 들어) 인터페이스를 통해 요구 사항을 정의하기 위해 혼합하여 일치시킨 다음 일부 또는 전부를 처리하는 구현을 가질 수 있습니다. 여전히 앱을 빌드하는 동안 사용할 모든 구현을 주입하십시오.


4
@Paul 답장을 보내 주셔서 감사합니다. 나는 실제로 그 접근법을 시도했다. 내가 시도한 첫 번째 방법을 일반적으로 표현하는 방법을 알 수 없었습니다 GetById(). 내가 사용 하는가 IRepository<T, TId>, GetById(object id)또는 가정 사용을 GetById(int id)? 복합 키는 어떻게 작동합니까? ID로 일반적인 선택이 가치있는 추상화인지 궁금했습니다. 그렇지 않다면, 일반 리포지토리가 어색하게 표현해야 할 것은 무엇입니까? 그것은 인터페이스가 아니라 구현을 추상화하는 데 대한 추론의 선이었다 .
Bryan Watts

10
또한 일반적인 쿼리 메커니즘은 ORM의 책임입니다. 리포지토리는 일반 쿼리 메커니즘 을 사용하여 프로젝트 엔터티에 대한 특정 쿼리를 구현해야합니다 . 리포지토리 소비자는보고와 같이 문제 도메인의 일부가 아닌 경우 자체 쿼리를 작성하도록 강요해서는 안됩니다.
Bryan Watts

2
@Bryan-GetById ()에 관해. FindById <T, TId> (TId id)를 사용합니다. 따라서 repository.FindById <Invoice, int> (435)와 같은 결과가 발생합니다.
Joshua Hayes

솔직히 일반 인터페이스에 필드 레벨 쿼리 메소드를 넣지는 않습니다. 지적했듯이 모든 모델을 단일 키로 쿼리해서는 안되며 경우에 따라 앱이 ID로 무언가를 검색하지 않을 수도 있습니다 (예 : DB 생성 기본 키를 사용하고 있고 고유 키 (예 : 로그인 이름). 쿼리 방법은 리팩토링의 일부로 구축 한 특정 인터페이스에서 발전합니다.
Paul

13

재정의 가능한 메소드 서명을 사용하여 일반 리포지토리 (또는 정확한 동작을 지정하는 일반 리포지토리 목록)에서 파생되는 특정 리포지토리를 선호합니다.


작은 발췌 예제를 제공해 주시겠습니까?
Johann Gerell

@Johann Gerell 아니오, 더 이상 리포지토리를 사용하지 않기 때문입니다.
Arnis Lapsa

리포지토리에서 멀리 떨어져 있으므로 지금 무엇을 사용하십니까?
Chris

@Chris 저는 풍부한 도메인 모델을 만드는 데 집중하고 있습니다. 응용 프로그램의 입력 부분이 중요합니다. 모든 상태 변경 사항을주의 깊게 모니터링하는 경우 데이터가 충분히 효율적이면 데이터를 읽는 방법은 그다지 중요하지 않습니다. NHibernate ISession을 직접 사용합니다. 저장소 계층 추상화가 없으면 열망하는 로딩, 다중 쿼리 등과 같은 항목을 훨씬 쉽게 지정할 수 있으며 실제로 필요한 경우 ISession을 조롱하는 것도 어렵지 않습니다.
Arnis Lapsa

3
@JesseWebb Naah ... 풍부한 도메인 모델 쿼리 로직을 사용하면 크게 단순화됩니다. 예를 들어 무언가를 구매 한 사용자를 검색하려면 사용자 대신 사용자를 검색하기 만하면됩니다. 순서 => order.Status == 1) .Join (X => x.products) 어디에요 (X .... 등 등 등 .... ㅋ 저쩌구
아르 니스 Lapsa

5

특정 저장소로 랩핑 된 일반 저장소가 있어야합니다. 이렇게하면 공용 인터페이스를 제어 할 수 있지만 일반 리포지토리를 사용하여 코드를 재사용 할 수 있다는 이점이 있습니다.


2

공개 클래스 UserRepository : 리포지토리, IUserRepository

인터페이스 노출을 피하기 위해 IUserRepository를 삽입해서는 안됩니다. 사람들이 말했듯이 전체 CRUD 스택 등이 필요하지 않을 수도 있습니다.


2
이것은 답변보다 주석처럼 보입니다.
Joshi Amit
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.