仓储模式:模型关系的实现和延迟加载

11

我有一个应用程序,涉及产品和产品类别。对于每个产品和产品类别,我都使用 POCO 定义了模型。

// Represents a product.
class Product {
  public virtual int ID { get; set; }
  public virtual string Name { get; set; }
  public virtual ProductCategory Category { get; set; }
}

// Represents a product category.
class ProductCategory {
  public virtual int ID { get; set; }
  public virtual string Name { get; set; }
  public virtual IEnumerable<Product> Products { get; set; }
}
应用程序使用存储库访问这些模型。
// The interface implemented by the application's repository
interface IProductRepository {
  IEnumerable<Product> GetAllProducts();

  void Add(Product product);
  void Remove(Product product);
  void Save(Product product);
}
在Product类中,类型为ProductCategory的名为Category的属性应该仅在需要/访问时加载(延迟加载)。我想让我的模型保持POCO,并且仅包含模型的结构。
我采取的方法正确吗?在Product类中,我应该只有类别的ID,并使用单独的存储库来加载类别吗?
实现延迟加载和关系。目前,我的存储库接口的实现返回一个类型扩展了Product类型并通过存储库实例支持延迟加载的对象。
谁应该负责加载产品类别?
我想知道产品和类别存储库应如何交互以实现延迟加载?它们是否应该相互引用,还是我应该有一个主存储库和两个子存储库,并将其传递给我的扩展模型类型?
你会采取什么方法?(欢迎任何建议和批评)
我应该指出,我希望应用程序具有可扩展性,并且所有存储库和模型本身的接口都将在单独的程序集中。这意味着扩展者将无法直接访问模型类定义。
4个回答

6
一些备注和我的意见:
1)不应该只在 Product 类中使用类别 ID,并使用单独的产品类别存储库来加载类别。您正在使用 ORM(至少我假设您正在使用),以便能够通过类实例之间的引用来建模关系,而不是通过查询关系方式使用ID。将您的想法推到最后的结果意味着您完全从模型类中删除了所有导航属性,并且只有标量属性,其中一些作为对象之间的键。这只是ORM中的“R”。
2)目前,我的存储库接口实现返回一个扩展 Product 类型并通过存储库实例支持延迟加载的对象。不确定这确切意味着什么(我想看看您如何执行代码片段)。但我猜测在您派生的 Product 类中,您以某种方式注入了对存储库的引用,例如:
public class ProductProxy : Product
{
    private IProductRepository _productRepo;

    public ProductProxy(IProductRepository productRepo)
    {
        _productRepo = productRepo;
    }

    // now you use _productRepo to lazily load something on request, do you?
}

显然现在加载类别是一个问题,因为 IProductRepository 没有访问它们的方法。

3)

我想知道产品和类别存储库应该如何相互交互以实现延迟加载?它们应该相互引用还是我应该有一个主存储库,其中包含两个子存储库,并将其传递给我的扩展模型类型?

您的 ProductRepository 和 CategoryRepository 看起来像是通用存储库的实例,仅负责单个实体类型(在 EF 4.1 中,这类似于 DbSet<T>,其中 T 分别为 ProductCategory)。

我建议避免这些存储库之间的引用,因为每当您添加新实体或导航属性时,这可能会导致复杂的 repo-references。

我看到另外两个选项:

  • (Basically what you already mentioned) Having a repository which is responsible for Product and Category together. You could still have your generic repositories but I would consider them more as internal helper repos and would only use them as private members inside of the main repository. This way you can have a group of repositories, each of them is responsible for some closely related entities.

  • Introduce a Unit of Work which is able to create all of your generic repositories (again in EF 4.1 this would be something like the factory method DbContext.Set<T>() where DbContext is the unit of work) and then inject this Unit of Work into your derived instances:

    public class ProductProxy : Product
    {
        private IUnitOfWork _unitOfWork;
    
        public ProductProxy(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }  
    
        public Category Category
        {
            get
            {
                // ...
                var productRepo = _unitOfWork.CreateGenericRepo<Product>();
                var categoryRepo = _unitOfWork.CreateGenericRepo<Category>();
                // you can pull out the repos you need and work with them
            }
            set { ... }
        }
    }
    

我更倾向于第二个选项,因为在第一个选项中,您可能会最终需要支持加载所有可能的关系的大型存储库。考虑一下:订单有订单项,订单项有产品,产品有类别,订单有客户,客户有地址列表,地址有联系人列表等等...

4)(因为您也在寻求批评意见)

您是在编写自己的ORM还是编写应用程序?根据我的看法,您的设计走向可能变得非常复杂,并且您正在重新发明轮子。如果您计划使用EF或NHibernate(或其他ORM),则您正在创建已经提供了开箱即用的功能,您只需要在其上添加抽象,这并不增加任何价值。通过动态代理进行延迟加载在透明的情况下发生,您从未在代码中显式地使用这些代理,您始终使用您的POCO实体。它们是不可见的,仅在运行时存在。为什么要开发自己的延迟加载基础设施?


#2. 你的假设是完全正确的,那正是我想要做的。 #4. 我正在尝试通过提供一个包含所需接口和模型以实现存储库(比此示例复杂得多)的.dll,并使用MEF加载实现来使应用程序可扩展。对于内部实现,我正在使用EF Code First,因此我的存储库将是干净的(将艰苦的工作留给EF)。非常感谢您的出色回答! - Cristian Toma

2
NHibernate通过代理对象(默认使用Castle DynamicProxy)支持延迟加载,这些代理对象是对存储库操作的类进行子类化。NHibernate要求您将成员标记为virtual以支持此场景。我不确定在需要时哪个组件会启动加载调用,但怀疑是代理实例。
我个人认为,将成员标记为virtual是获得延迟加载功能的小代价。

@Russ 我知道这些框架(还有实体代码优先),这就是为什么我将模型中的所有成员标记为虚拟的原因,但我也想知道存储库应该如何交互? - Cristian Toma
4
理想情况下,你应该只为聚合根拥有存储库。请参考https://dev59.com/43I-5IYBdhLWcg3wO1rl。因此,假设您的存储库加载带有类别代理的“Product”实例,在调用该属性的“get”时,代理应从数据库中加载真正的实例并将其分配给该属性。也许代理对象应该在内部保存与其相关的聚合根的ID。 - Russ Cam
我明白,但是我应该如何管理ProductsRepository和ProductCategoriesRepository实例并将它们传递给模型?它们应该放在哪里才能相互引用? - Cristian Toma
此实现与环境无关。特定于存储库的模型(即扩展基本模型类型的类型,由扩展器实现)需要引用存储库,以便它们可以实现延迟加载。再次强调,我的问题是关于ProductsRepository和ProductCategoriesRepository实例所在的位置,以便从存储库检索产品时将类别存储库的实例传递给它,以便在需要时通过它加载适当的类别。我希望这能更清楚地说明我想知道的内容,谢谢。 - Cristian Toma
但是那我不会有重复的检索类别代码吗?在类别存储库和扩展/代理模型本身中都有?@Slauma的回答解答了我的问题。也谢谢你! - Cristian Toma
显示剩余2条评论

0

我正在做类似的事情,我的情况是我有产品和产品类别的存储库。除了存储库之外,没有人知道如何加载东西 - 惰性或其他 - 因此如果您想要更改它,可以稍后更改。在我的情况下,我急切地加载类别,因为它们被访问得如此频繁,而我按需加载产品并将其缓存一小时左右。


0
我希望我的模型保持POCO,仅包含模型的结构。
我想问一下,为什么这些类的数据形状很重要?
通常,这意味着您只是直接向整个应用程序公开数据库架构,并在其上添加了非常薄的“模型”层。
我建议实体包含的数据是私有的,应该封装起来,而行为才是实体的重点。
此外,一次加载所有需要的数据更简单、更清晰。如果潜在的数据量太大,那么我建议您将一个实体分配给过多的职责,需要进一步完善您的模型或添加一种机制,以根据使用它的上下文检索实体。

我的意思是,如果您正在查询以显示数据,则需要公开一些数据集。在另一个上下文中,您可能会查询类似但不同的数据集。更进一步地,如果您需要执行操作或修改,则实体需要公开特定行为。但在不同的上下文中 - 当您检索实体以执行不同的操作或修改时,您需要公开不同的特定行为。

试图将所有这些不同的用途合并为一个单一的合同或接口,更多或少导致了上帝对象实体。

您只需要在某些显示和某些操作中使用那些类别集合。尝试始终公开它只会带来痛苦。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接