如果实体彼此相关,存储库模式如何工作?

时间:2011-11-12 08:21:30

标签: .net linq-to-sql repository-pattern

question about IRepository及其用途,有一个看似很好的答案。

我的问题是:我如何干净地处理彼此相关的实体,而不是IRepository然后只是一个没有实际目的的层?

假设我有这些业务对象:

public class Region {
    public Guid InternalId {get; set;}
    public string Name {get; set;}
    public ICollection<Location> Locations {get; set;}
    public Location DefaultLocation {get; set;}
}

public class Location {
    public Guid InternalId {get; set;}
    public string Name {get; set;}
    public Guid RegionId {get; set;}
}

有规则:

  • 每个地区必须至少有一个地点
  • 使用位置
  • 创建新创建的区域
  • 请不要选择N + 1

那么我的RegionRepository会是什么样子?

public class RegionRepository : IRepository<Region>
{
    // Linq To Sql, injected through constructor
    private Func<DataContext> _l2sfactory;

    public ICollection<Region> GetAll(){
         using(var db = _l2sfactory()) {
             return db.GetTable<DbRegion>()
                      .Select(dbr => MapDbObject(dbr))
                      .ToList();
         }
    } 

     private Region MapDbObject(DbRegion dbRegion) {
         if(dbRegion == null) return null;

         return new Region {
            InternalId = dbRegion.ID,
            Name = dbRegion.Name,
            // Locations is EntitySet<DbLocation>
            Locations = dbRegion.Locations.Select(loc => MapLoc(loc)).ToList(),
            // DefaultLocation is EntityRef<DbLocation>
            DefaultLocation = MapLoc(dbRegion.DefaultLocation)
         }
     }

     private Location MapLoc(DbLocation dbLocation) {
         // Where should this come from?
     }
}

如您所见,RegionRepository也需要获取位置。在我的示例中,我使用Linq To Sql EntitySet / EntiryRef,但现在Region需要处理将Locations映射到Business Objects(因为我有两组对象,业务和L2S对象)。

我应该将此重构为:

public class RegionRepository : IRepository<Region>
{
    private IRepository<Location> _locationRepo;

    // snip

    private Region MapDbObject(DbRegion dbRegion) {
         if(dbRegion == null) return null;

         return new Region {
            InternalId = dbRegion.ID,
            Name = dbRegion.Name,
            // Now, LocationRepo needs to concern itself with Regions...
            Locations = _locationRepo.GetAllForRegion(dbRegion.ID),
            // DefaultLocation is a uniqueidentifier
            DefaultLocation = _locationRepo.Get(dbRegion.DefaultLocationId)
         }  
  }

现在我很好地将我的数据层分成了原子库,每个只处理一种类型。我启动了Profiler和......哎呀,选择N + 1。因为每个Region都调用位置服务。我们只有十几个地区和40个左右的位置,因此自然优化是使用DataLoadOptions。问题是RegionRepository不知道LocationRepository是否使用相同的DataContext。毕竟我们在这里注入工厂,所以LocationRepository可能会自己开发它。即使它没有 - 我正在调用提供业务对象的服务方法,因此无论如何都不能使用DataLoadOptions。

啊,我忽略了什么。 IRepository应该有这样的方法:

public IQueryable<T> Query()

所以现在我会做

         return new Region {
            InternalId = dbRegion.ID,
            Name = dbRegion.Name,
            // Now, LocationRepo needs to concern itself with Regions...
            Locations = _locationRepo.Query()
                        .Select(loc => loc.RegionId == dbRegion.ID)
                        .ToList(),
            // DefaultLocation is a uniqueidentifier
            DefaultLocation = _locationRepo.Get(dbRegion.DefaultLocationId)
         }  

看起来不错。首先。在第二次检查时,我有单独的业务和L2S对象,所以我仍然看不到这是如何避免SELECT N + 1,因为Query不能只返回GetTable<DbLocation>

问题似乎是有两组不同的对象。但是如果我使用所有System.Data.LINQ属性([Table],[Column]等)来装饰Business Objects,那就会打破抽象并破坏IRepository的目的。因为也许我想也能够使用其他ORM,此时我现在必须用其他属性来装饰我的业务实体(同样,如果业务实体在一个单独的.Business程序集中,它的消费者现在需要引用所有ORM以及要解析的属性 - 哎!)。

对我来说,似乎IRepository应该是IService,上面的类应该是这样的:

public class RegionService : IRegionService {
      private Func<DataContext> _l2sfactory;

      public void Create(Region newRegion) {
        // Responsibility 1: Business Validation
        // This could of course move into the Region class as
        // a bool IsValid(), but that doesn't change the fact that
        // the service concerns itself with validation
        if(newRegion.Locations == null || newRegion.Locations.Count == 0){
           throw new Exception("...");
        }

        if(newRegion.DefaultLocation == null){
          newRegion.DefaultLocation = newRegion.Locations.First();
        }

        // Responsibility 2: Data Insertion, incl. Foreign Keys
        using(var db = _l2sfactory()){
            var dbRegion = new DbRegion {
                ...
            }

            // Use EntitySet to insert Locations as well
            foreach(var location in newRegion.Locations){
                var dbLocation = new DbLocation {

                }
                dbRegion.Locations.Add(dbLocation);
            }

            // Insert Region AND all Locations
            db.InsertOnSubmit(dbRegion);
            db.SubmitChanges();
        }
      }
}

这也解决了鸡蛋问题:

  • DbRegion.ID由数据库生成(作为newid())并且设置了IsDbGenerated = true
  • DbRegion.DefaultLocationId是一个不可为空的GUID
  • DbRegion.DefaultLocationId是一个FK到Location.ID
  • DbLocation.RegionId是一个不可为空的GUID,一个FK到Region.ID

在没有EntitySet的情况下执行此操作几乎是不可能的,因此,除非您牺牲数据库上的数据完整性并将其移至业务逻辑中,否则无法对Region提供商的Locations负责。

我看到这个帖子如何被视为不是一个真实的问题,主观和议论,所以请允许我提出一个客观的问题:

  • 存储库模式应该抽象出来的究竟是什么?
  • 在现实世界中,人们如何在不破坏存储库模式应该实现的抽象的情况下优化数据库层?
  • 具体来说,现实世界如何处理SELECT N + 1和数据完整性问题?

我想我的真正问题是:

  • 当已经使用ORM(比如Linq To Sql)时,DataContext不是我的存储库,因此DataContext上的存储库只是抽象同样的东西再次

2 个答案:

答案 0 :(得分:4)

在设计存储库时,您应该考虑所谓的聚合根。从本质上讲,这意味着如果一个实体可以在域内单独存在,那么它将不仅仅拥有它自己的存储库。在你的情况下,这将是地区。

考虑典型的客户/订单方案。客户存储库将提供对订单的访问,因为订单在没有客户的情况下不能存在,因此除非您有有效的业务案例,否则您不太可能需要单独的订单存储库。

在一个简单的应用程序中,您的假设可能是正确的,但请记住,除非您提供L2S上下文的抽象,否则您将难以执行有效的单元测试。对接口进行编码,无论是IServiceX,IRepositoryX还是其他什么都可以为您提供这种级别的分离。

关于服务接口是否进入设计的决定通常与业务逻辑的复杂性以及对可能由多个不同客户端消耗的可扩展Api进入该逻辑的需求有关。

答案 1 :(得分:1)

我对这一切有几点想法: 1. AFAIK存储库模式比ORM早一点发明。回到普通SQL查询的日子里,实现Repository是一个不错的主意,并从使用的实际数据库中获取这个抽象代码。 2.我可以说现在完全不需要存储库,但不幸的是,根据我的经验,我不能说,任何ORM都可以真正从所有数据库细节中抽象出来。例如。我无法创建一个ORM映射,只是将它与任何其他数据库服务器一起使用,ORM声称支持(特别是我在谈论Microsoft EF)。因此,如果您真的希望能够使用不同的数据库服务器,那么您仍然需要使用Repository。 另一个问题很简单:代码重复。当然,有些查询会经常调用您的代码。如果您只将ORM作为存储库,那么您将复制这些查询,因此最好对ORM容器进行一定程度的抽象,这将保留那些常用的查询。