将数据访问层(DAL)与Entity Framework实现分离

4

首先,如果这篇文章有点长,请原谅。但是如果没有提供足够的细节,我就无法正确地解释问题。

我在寻找一种方法来将我的数据访问层(DAL)与Entity Framework实现进行解耦。虽然我正在从事的项目非常小,但如果将来我想切换到另一个ORM,比如NHibernate或纯粹的ADO.NET,我希望只需为实现编写代码,而不是整个DAL。

假设我在MyWallet.DAL中有以下实体:

public interface IWallet {
    long Id { get; set; }
    float TotalAmountOfMoney { get; set; }
    long CurrencyId { get; set; }
    ICurrency Currency { get; set; }
    DateTime RecordedOn { get; set; }
    ICollection<IMoneyMovement> MoneyMovements { get; set; }
}

public interface ICurrency {
    long Id { get; set; }
    char Symbol { get; set; }
    string Code { get; set; }
    string Description { get; set; }
}

public interface IMoneyMovement {
    long Id { get; set; }
    float Amount { get; set; }
    string Description { get; set; }
    long WalletId { get; set; }
    IWallet Wallet { get; set; }
    DateTime RecordedOn { get; set; }
    DateTime MovedOn { get; set; }
}

如您所见,这些是普通的接口,我计划在另一个库中实现它们,该库将包含实际的Entity Framework实现(比如MyWallet.DAL.EntityFramework)。当然,我会使用Entity Framework特定的属性(例如[Key]或[ForeignKey]等)来装饰实体实现。
我还在MyWallet.DAL中定义了一些存储库,如IWalletRepository、IMoneyMovementRepository、ICurrencyRepository,以便访问实体。实际上,我不知道这是否是设计访问实体的正确方式。当然,我也定义了工厂来获取实体的具体实现。
在我的业务层中,我定义了服务来处理对象请求,与DAL实体一起工作,并返回业务对象,就像这样:
public class WalletService {
    private readonly IWalletRepository _walletRepository;
    private readonly IWalletFactory _walletFactory;

    public WalletService(IWalletRepository walletRepository, 
        IWalletFactory walletFactory) {

        _walletRepository = walletRepository;
        _walletFactory = walletFactory;
    }

    public CreatedWallet CreateWallet(CreateWalletRequest request) {
        var wallet = _walletFactory.Create();

        wallet.CurrencyId = request.CurrencyId;
        wallet.TotalAmountOfMoney = request.TotalAmountOfMoney;
        wallet.RecordedOn = DateTime.Now;

        _walletRepository.Create(wallet);
        _walletRepository.SaveChanges();

        return new CreatedWallet {
            Id = wallet.Id
        }
    }
}

我原以为这会无缝运行,或者至少在我有多个存储库的情况下,我可以共享DataContext,这样我只需要在一个上触发SaveChanges方法就可以反映数据库中的更改。
问题出在存储库实现上,在这种情况下,我将继续使用Entity Framework:
public class EFDataContext : DbContext {
    public EFDataContext() : base ("name=MyConnectionString") {
    }

    public virtual DbSet<EFWallet> Wallets { get; set; }
    public virtual DbSet<EFMoneyMovement> MoneyMovements { get; set; }
    public virtual DbSet<EFCurrency> Currencies { get; set; }
}

public class EFWalletRepository : IWalletRepository {
    private readonly EFDbContext _dataContext;

    public EFWalletRepository(EFDbContext dataContext) {
        _dataContext = dataContext ?? new EFDbContext();
    }

    public int SaveChanges() {
        return _dataContext.SaveChanges();
    }

    public void Dispose() {
        _dataContext.Dispose();
    }

    public void Create(IWallet wallet) {
        ...???
    }
}

现在的问题是:当DataContext只知道具体实现时,我如何与接口一起工作?我做错了吗?
更新: @TomTom指出,既然可以拥抱Entity Framework的力量,为什么还要与它对抗呢?我想我会让EF成为抽象层。事实上,通过让EF充当数据访问层,您可以专注于项目的业务逻辑。
至于存储库/工作单元问题,我可以将多个存储库包装在一个工作单元中,或者简单地让DbContext成为工作单元。
public class EFWalletRepository : IWalletRepository {
    private readonly EFDbContext _dataContext;

    public EFWalletRepository() {
        _dataContext = new EFDbContext();
    }

    public void Dispose() {
        _dataContext.Dispose();
    }

    public IEnumerable<Wallet> Wallets {
        get { return _dataContext.Wallets; }
    }

    public void SaveWallet(Wallet wallet) {
        if (wallet.Id == 0) {
            _dataContext.Wallets.Add(wallet);
        } else {
            var databaseEntry = _dataContext.Wallets.Find(wallet.Id);
            //update properties
        }

        _dataContext.SaveChanges();
    }
}

2
你也在抽象化数据;你应该只抽象化逻辑。将POCO保留在业务层。 - Maarten
2个回答

2
简单来说:是的,你做错了。你引入了一个不好的抽象(在功能上使你付出了代价)“因为”。EF本身就是一个抽象。
在它之上的任何抽象都会在使用功能方面造成损失 - 在数据库方面,这将带来巨大的性能影响。想要一个例子吗?“Include”用于预加载导航属性(而不是延迟加载)。你将不得不绕过这个问题以及更多ORM特定的详细行为 - 为了获得什么?如果你放弃那些更高级的、更具体的功能,你的性能就会受到影响。

我认为这取决于他的需求。在我需要它们之前,我不会涵盖这些功能YAGNI。主要问题是最大的商业价值在哪里?拥有更简单的更改dbms的方法还是拥有更容易打开EF功能的窗口。EF只是一个“半路”抽象,因为它强烈依赖于EF技术本身。这本质上并不坏,但它是一种依赖关系。我认为两种方式都是可能的,并且应该根据需求进行验证。 - Boas Enkler
我不这么认为。在我的情况下,我从来没有需要过这个EF功能,而且很可能我永远也不会需要它们。如果这些功能显而易见,那么你是对的。但是,在他的情况下,它们真的吗?但是你粗鲁的帖子让我感到有点奇怪,也不太专业... 实现一切只是因为害怕“也许在10年后我可能需要它”是非常糟糕的。如果你知道你需要它,那就实现它。但是,如果不知道他的上下文,你就无法知道。最好建议他考虑是否非常可能需要此功能或询问商人! - Boas Enkler
好的,也许我之前应该说明一下,但是我正在工作的这个项目只是“家庭作业”,只是通过从一个非常小的起点开始构建东西来提高设计技能的一种方式。当然,这个项目根本不需要这种抽象,而且在这种情况下,EF本身就是一种抽象,即使不是我所想象的那种抽象。 - lucacelenza
@BoasEnkler 嗯,Boas,那很可能是无能的例子,因为我在超过50%的查询中都使用了这个特定的功能。所以,要么我只处理非常复杂的查询,要么你基本上会陷入我在许多项目团队中看到的陷阱:人们不知道自己在做什么,一旦有人展示给他们适当的性能,他们就感到惊讶。似乎有很多基线功能你从来没有使用过。好吧,对你来说很好。我们中的一些人必须专业地工作,因为应用程序需要它。 - TomTom

1
我看不出抽象你的模型(实体)有任何理由。你希望当你改变访问数据库的方式时,它们会发生变化吗?
但是,如果您想保持这种方式,您可以使存储库接口成为通用的,并在定义存储库时传递具体的实体类型,这样您就会得到:
public class EFWalletRepository : IWalletRepository<EFWallet>
{
    public void Create(EFWallet wallet)
    {
        _dataContext.Add(wallet);
    }
}

其他建议:

  1. 您不应该将集合暴露在模型属性中。这违反了面向对象编程的规则 - 您应该暴露一些方法来操作对象,状态应该更加内部化。
  2. 您可能不应该在存储库中添加 SaveChanges() 方法 - 这应该是一个“工作单元”任务,以提交所有更改到数据库。
  3. 如果您在服务层中使用多个存储库,您将遇到一个问题,因为当您应该拥有一个单独的“工作单元”时,为存储库创建一个新的 DbContext

我正在抽象实体,因为EF有自己的装饰器,我不想在我的DAL库中包含它们。 `[Table("wallet")] public class Wallet { [Key] [Column("id")] public long Id { get; set; } .... }` - lucacelenza
1
不必使用属性来装饰实体,你可以使用配置文件。这样,你的实体将保持 EF-free 和清洁,参见此处:http://www.entityframeworktutorial.net/code-first/fluent-api-in-code-first.aspx - tdragon

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