IT story

작업 단위 + 리포지토리 패턴 : 비즈니스 트랜잭션 개념의 몰락

hot-time 2020. 12. 31. 22:57
반응형

작업 단위 + 리포지토리 패턴 : 비즈니스 트랜잭션 개념의 몰락


결합 Unit of Work및 것은 Repository Pattern매우 널리 요즘 사용하는 무언가이다. Martin Fowler가 말했듯 이 사용 목적은 저장소가 실제로 작동하는 방식 (지속적인 무지)에 대해 무지하면서 비즈니스 트랜잭션UoW 을 형성하는 것 입니다. 많은 구현을 검토했습니다. 특정 세부 사항 (콘크리트 / 추상 클래스, 인터페이스 등)을 무시하면 다음과 비슷합니다.

public class RepositoryBase<T>
{
    private UoW _uow;
    public RepositoryBase(UoW uow) // injecting UoW instance via constructor
    {
       _uow = uow;
    }
    public void Add(T entity)
    {
       // Add logic here
    }
    // +other CRUD methods
}

public class UoW
{
    // Holding one repository per domain entity

    public RepositoryBase<Order> OrderRep { get; set; }
    public RepositoryBase<Customer> CustomerRep { get; set; }
    // +other repositories

    public void Commit()
    {
       // Psedudo code: 
       For all the contained repositories do:
           store repository changes.
    }
}

이제 내 문제 :

UoWCommit변경 사항을 저장하는 공용 메서드 노출합니다 . 또한, 각 저장소는 공유 인스턴스 때문에 UoWRepositoryCAN 액세스 방법 CommitUOW 온. 한 저장소에서 호출하면 다른 모든 저장소에도 변경 사항이 저장됩니다. 따라서 결과적으로 트랜잭션의 전체 개념이 무너집니다.

class Repository<T> : RepositoryBase<T>
{
    private UoW _uow;
    public void SomeMethod()
    {
        // some processing or data manipulations here
        _uow.Commit(); // makes other repositories also save their changes
    }
}

나는 이것이 허용되지 않아야한다고 생각한다. UoW(비즈니스 트랜잭션) 의 목적을 고려할 때 비즈니스 계층과 Commit같은 비즈니스 트랜잭션시작한 사람에게만 메소드 가 노출되어야합니다 . 저를 놀라게 한 것은이 문제를 다루는 기사를 찾을 수 없다는 것입니다. 그들 모두 Commit에서 주입되는 모든 저장소에서 호출 수 있습니다.

추신 : 내가 전화하지 내 개발자를 알 수 있습니다 알고 CommitA의 Repository하지만,이 아키텍처는 신뢰할 수있는 개발자보다 더 신뢰성이 신뢰할!


나는 당신의 우려에 동의합니다. 나는 작업 단위를 여는 가장 바깥 쪽 함수가 커밋 또는 중단 여부를 결정하는 작업 단위를 선호합니다. 호출 된 함수는 주변 UoW가있는 경우 자동으로 참여하는 작업 단위 범위를 열 수 있고,없는 경우 새로 생성 할 수 있습니다.

UnitOfWorkScope내가 사용한 의 구현은 TransactionScope작동 방식에서 크게 영감을 받았습니다 . 주변 / 범위 접근 방식을 사용하면 종속성 주입이 필요하지 않습니다.

쿼리를 수행하는 메서드는 다음과 같습니다.

public static Entities.Car GetCar(int id)
{
    using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Reading))
    {
        return uow.DbContext.Cars.Single(c => c.CarId == id);
    }
}

작성하는 방법은 다음과 같습니다.

using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Writing))
{
    Car c = SharedQueries.GetCar(carId);
    c.Color = "White";
    uow.SaveChanges();
}

uow.SaveChanges()호출은 루트 (가장 마지막) 범위 인 경우에만 데이터베이스에 실제 저장을 수행합니다. 그렇지 않으면 루트 범위가 변경 사항을 저장할 수 있다는 "괜찮은 투표"로 해석됩니다.

의 전체 구현은 http://coding.abel.nu/2012/10/make-the-dbcontext-ambient-with-unitofworkscope/UnitOfWorkScope 에서 사용할 수 있습니다 .


저장소를 UoW의 구성원으로 만드십시오. 리포지토리가 UoW를 '보도록'하지 마십시오. UoW가 트랜잭션을 처리하도록합니다.


를 전달하지 말고 UnitOfWork필요한 메소드가있는 인터페이스를 전달하십시오. UnitOfWork원하는 경우 원래의 구체적인 구현 에서 해당 인터페이스를 구현할 수 있습니다 .

public interface IDbContext
{
   void Add<T>(T entity);
}

public interface IUnitOfWork
{
   void Commit();
}

public class UnitOfWork : IDbContext, IUnitOfWork
{
   public void Add<T>(T entity);
   public void Commit();
}

public class RepositoryBase<T>
{
    private IDbContext _c;

    public RepositoryBase(IDbContext c) 
    {
       _c = c;
    }

    public void Add(T entity)
    {
       _c.Add(entity)
    }
}

편집하다

이것을 게시 한 후 다시 생각했습니다. UnitOfWork구현 에서 Add 메서드를 노출 한다는 것은 두 패턴의 조합임을 의미합니다.

나는 내 자신의 코드에서 Entity Framework를 DbContext사용하고 거기 에서 사용되는 것은 "작업 단위와 리포지토리 패턴의 조합"으로 설명됩니다.

두 개를 분할하는 것이 더 낫다고 생각합니다. 즉 DbContext, Unit Of Work 비트에 대해 하나와 Repository 비트에 대해 하나씩 두 개의 래퍼가 필요합니다 . 그리고 RepositoryBase.

주요 차이점은 UnitOfWork리포지토리에를 전달하지 않고 DbContext. 이는에 BaseRepository대한 액세스 권한 이 있음을 의미 SaveChanges합니다 DbContext. 의도는 사용자 지정 리포지토리가 상속해야 BaseRepository하기 때문에 DbContext에도 액세스 할 수 있습니다. 따라서 개발자 이를 사용하는 사용자 정의 저장소에 코드를 추가 할 수 있습니다 DbContext. 그래서 내 "래퍼"가 약간 새는 것 같아요 ...

그렇다면 DbContext저장소 생성자에게 전달할 수있는 다른 래퍼를 만들어 닫을 가치가 있습니까? 확실하지 않습니다 ...

DbContext 전달의 예 :

저장소 및 작업 단위 구현

Entity Framework의 리포지토리 및 작업 단위

John Papa의 원본 소스 코드


이 질문을받은 지 오래되었고, 사람들이 노년에 사망하고 경영진으로 옮겨 졌을 수도 있지만 여기에 있습니다.

데이터베이스, 트랜잭션 컨트롤러 및 2 단계 커밋 프로토콜에서 영감을 얻어 패턴에 대한 다음 변경 사항이 적용됩니다.

  1. Fowler의 EAA 책 P에 설명 된 작업 단위 인터페이스를 구현하되 각 UoW 메소드에 저장소를 삽입하십시오.
  2. 각 저장소 작업에 작업 단위를 삽입합니다.
  3. 각 저장소 작업은 적절한 UoW 작업을 호출하고 자체 주입합니다.
  4. 저장소에서 2 단계 커밋 메서드 CanCommit (), Commit () 및 Rollback ()을 구현합니다.
  5. 필요한 경우 UoW에 대한 커밋은 각 저장소에서 커밋을 실행하거나 데이터 저장소 자체에 커밋 할 수 있습니다. 원하는 경우 2 단계 커밋을 구현할 수도 있습니다.

이렇게하면 리포지토리와 UoW를 구현하는 방법에 따라 다양한 구성을 지원할 수 있습니다. 예를 들어 트랜잭션이없는 단순 데이터 저장소, 단일 RDBM, 여러 이기종 데이터 저장소 등에서 데이터 저장소와 상호 작용은 상황에 따라 저장소 또는 UoW에있을 수 있습니다.

interface IEntity
{
    int Id {get;set;}
}

interface IUnitOfWork()
{
    void RegisterNew(IRepsitory repository, IEntity entity);
    void RegisterDirty(IRepository respository, IEntity entity);
    //etc.
    bool Commit();
    bool Rollback();
}

interface IRepository<T>() : where T : IEntity;
{
    void Add(IEntity entity, IUnitOfWork uow);
    //etc.
    bool CanCommit(IUnitOfWork uow);
    void Commit(IUnitOfWork uow);
    void Rollback(IUnitOfWork uow);
}

사용자 코드는 DB 구현에 관계없이 항상 동일하며 다음과 같습니다.

// ...
var uow = new MyUnitOfWork();

repo1.Add(entity1, uow);
repo2.Add(entity2, uow);
uow.Commit();

원래 게시물로 돌아갑니다. 우리는 각 저장소 작업에 UoW를 주입하는 방법이므로 각 저장소에 UoW를 저장할 필요가 없습니다. 즉, 저장소의 Commit ()은 실제 DB 커밋을 수행하는 UoW의 Commit으로 스텁 아웃 될 수 있습니다.


.NET에서 데이터 액세스 구성 요소는 일반적으로 주변 트랜잭션에 자동으로 참여합니다. 따라서 트랜잭션 내 변경 사항을 저장 하는 것은 변경 사항을 유지하기 위해 트랜잭션을 커밋하는 것과 분리 됩니다 .

다르게 말하면-트랜잭션 범위를 만들면 개발자가 원하는만큼 저장하도록 할 수 있습니다. 트랜잭션이 커밋 될 때까지 데이터베이스의 관찰 가능한 상태가 업데이트되지 않습니다 (볼 수있는 것은 트랜잭션 격리 수준에 따라 다름).

다음은 C #에서 트랜잭션 범위를 만드는 방법을 보여줍니다.

using (TransactionScope scope = new TransactionScope())
{
    // Your logic here. Save inside the transaction as much as you want.

    scope.Complete(); // <-- This will complete the transaction and make the changes permanent.
}

저도 최근에이 디자인 패턴을 연구하고 있으며 작업 단위 (UOW) 및 일반 저장소 패턴을 활용하여 저장소 구현을위한 작업 단위 "변경 사항 저장"을 추출 할 수있었습니다. 내 코드는 다음과 같습니다.

public class GenericRepository<T> where T : class
{
  private MyDatabase _Context;
  private DbSet<T> dbset;

  public GenericRepository(MyDatabase context)
  {
    _Context = context;
    dbSet = context.Set<T>();
  }

  public T Get(int id)
  {
    return dbSet.Find(id);
  }

  public IEnumerable<T> GetAll()
  {
    return dbSet<T>.ToList();
  }

  public IEnumerable<T> Where(Expression<Func<T>, bool>> predicate)
  {
    return dbSet.Where(predicate);
  }
  ...
  ...
}

기본적으로 우리가하는 일은 데이터 컨텍스트를 전달하고 기본 Get, GetAll, Add, AddRange, Remove, RemoveRange 및 Where에 대해 엔티티 프레임 워크의 dbSet 메서드를 활용하는 것입니다.

이제 이러한 메서드를 노출하는 일반 인터페이스를 만듭니다.

public interface <IGenericRepository<T> where T : class
{
  T Get(int id);
  IEnumerable<T> GetAll();
  IEnumerabel<T> Where(Expression<Func<T, bool>> predicate);
  ...
  ...
}

이제 우리는 엔티티 프레임 워크의 각 엔티티에 대한 인터페이스를 만들고 IGenericRepository에서 상속하여 인터페이스가 상속 된 리포지토리 내에서 구현 된 메서드 서명을 가질 것으로 예상합니다.

예:

public interface ITable1 : IGenericRepository<table1>
{
}

모든 엔티티에 대해 이와 동일한 패턴을 따릅니다. 또한 엔터티와 관련된 이러한 인터페이스에 함수 서명을 추가합니다. 이로 인해 저장소는 GenericRepository 메서드와 인터페이스에 정의 된 사용자 지정 메서드를 구현해야합니다.

Repositories의 경우 다음과 같이 구현합니다.

public class Table1Repository : GenericRepository<table1>, ITable1
{
  private MyDatabase _context;

  public Table1Repository(MyDatabase context) : base(context)
  {
    _context = context;
  }
} 

In the example repository above I am creating the table1 repository and inheriting the GenericRepository with a type of "table1" then I inherit from the ITable1 interface. This will automatically implement the generic dbSet methods for me, thus allowing me to only focus on my custom repository methods if any. As I pass the dbContext to the constructor I must also pass the dbContext to the base Generic Repository as well.

Now from here I will go and create the Unit of Work repository and Interface.

public interface IUnitOfWork
{
  ITable1 table1 {get;}
  ...
  ...
  list all other repository interfaces here.

  void SaveChanges();
} 

public class UnitOfWork : IUnitOfWork
{
  private readonly MyDatabase _context;
  public ITable1 Table1 {get; private set;}

  public UnitOfWork(MyDatabase context)
  {
    _context = context; 

    // Initialize all of your repositories here
    Table1 = new Table1Repository(_context);
    ...
    ...
  }

  public void SaveChanges()
  {
    _context.SaveChanges();
  }
}

I handle my transaction scope on a custom controller that all other controllers in my system inherit from. This controller inherits from the default MVC controller.

public class DefaultController : Controller
{
  protected IUnitOfWork UoW;

  protected override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    UoW = new UnitOfWork(new MyDatabase());
  }

  protected override void OnActionExecuted(ActionExecutedContext filterContext) 
  {
    UoW.SaveChanges();
  }
}

By implementing your code this way. Every time a request is made to the server at the beginning of an action a new UnitOfWork will be created and will automatically create all the repositories and make them accessible to the UoW variable in your controller or classes. This will also remove your SaveChanges() from your repositories and place it within the UnitOfWork repository. And last this pattern is able to utilize only a single dbContext throughout the system via dependency injection.

If you are concerned about parent/child updates with a singular context you could utilize stored procedures for your update, insert, and delete functions and utilize entity framework for your access methods.


Yes, this question is a concern to me, and here's how I handle it.

First of all, in my understanding Domain Model should not know about Unit of Work. Domain Model consists of interfaces (or abstract classes) that don't imply the existence of the transactional storage. In fact, it does not know about the existence of any storage at all. Hence the term Domain Model.

Unit of Work is present in the Domain Model Implementation layer. I guess this is my term, and by that I mean a layer that implements Domain Model interfaces by incorporating Data Access Layer. Usually, I use ORM as DAL and therefore it comes with built-in UoW in it (Entity Framework SaveChanges or SubmitChanges method to commit the pending changes). However, that one belongs to DAL and does not need any inventor's magic.

On the other hand, you are referring to the UoW that you need to have in Domain Model Implementation layer because you need to abstract away the part of "committing changes to DAL". For that, I would go with Anders Abel's solution (recursive scropes), because that addresses two things you need to solve in one shot:

  • You need to support saving of aggregates as one transaction, if the aggregate is an initiator of the scope.
  • You need to support saving of aggregates as part of the parent transaction, if the aggregate is not the initiator of the scope, but is part of it.

ReferenceURL : https://stackoverflow.com/questions/19548531/unit-of-work-repository-pattern-the-fall-of-the-business-transaction-concept

반응형