简体   繁体   English

实体框架和领域驱动设计

[英]Entity Framework and Domain Driven Design

After spending a few days trying to set up a simple application with EF and DDD I have to say that I feel quite frustrated and think that I was better off using Linq-to-SQL and forget all about DDD and EF.在花了几天时间尝试使用 EF 和 DDD 设置一个简单的应用程序后,我不得不说我感到非常沮丧,并认为我最好使用 Linq-to-SQL 而忘记了 DDD 和 EF。

With EF与英孚

a) You cannot have proper readonly collections a) 你不能有合适的只读集合

b) When you remove something from a collection of child items you quite often get that The relationship could not be changed because one or more of the foreign-key properties is non-nullable message b) 当您从一组子项中删除某些内容时,您经常会得到无法更改关系,因为一个或多个外键属性是不可为空的消息

c) There is no easy way of deleting all child items of a parent and reinserting them c) 没有简单的方法可以删除父项的所有子项并重新插入它们

All these are pretty much show stoppers for me given that the workarounds I have found are quite nasty looking.鉴于我发现的解决方法看起来很糟糕,所有这些对我来说都非常令人讨厌。 Has someone managed to put together a simple repository that addresses these issues?有人设法建立了一个简单的存储库来解决这些问题吗?

If yes would you be kind enough to share some code?!?如果是的话,您是否愿意分享一些代码?!?

Also, and I know this is a big topic, does anyone have any hands on experience of any real world DDD benefits in large scale web applications?另外,我知道这是一个很大的话题,有没有人在大型 Web 应用程序中体验过任何真实世界的 DDD 好处? We all know the theory but it would be nice to have an idea if it is actually worth the hassle!我们都知道这个理论,但是如果它真的值得麻烦的话,如果有一个想法会很好!


Ok, the best i can do so far without having to do all sorts of carzy workarounds is to use AsNoTracking() when i query something.好的,到目前为止,我可以做的最好的事情是在查询某些内容时使用 AsNoTracking()。 That way i get my info and EF leaves alone without doing whatever the hell it does behind my back.这样我就可以得到我的信息,EF 不会在背后做任何他妈的事。 I can now Remove from a collection and i can kind of be able to delete as well (who would think that id have to go back to sql fro this!) Does anyone know any pitfalls of using AsNoTracking?我现在可以从集合中删除,我也可以删除(谁会认为 id 必须为此返回 sql!)有谁知道使用 AsNoTracking 的任何陷阱? As far as i can genearate SQL based on my objects and populate them or update/delete them i am fine.至于我可以根据我的对象生成 SQL 并填充它们或更新/删除它们,我很好。 The whole tracking thing goes too far anyway?无论如何,整个跟踪事情都走得太远了?


namespace EShop.Models.Repositories
{
public class CustomerRepository : BaseRepository, IRepository<Customer, Int32>
{
    public CustomerRepository() : base(new EShopData()) { }

    #region CoreMethods

    public void InsertOrUpdate(Customer customer)
    {
        if (customer.CustomerId > 0)
        {
            // you cannot use remove, if you do you ll attach and then you ll have issues with the address/cards below
            // dbContext.Entry<CustomerAddress>(address).State = EntityState.Added; will fail
            dbContext.Database.ExecuteSqlCommand("DELETE FROM CustomerAddress WHERE CustomerId = @CustomerId", new SqlParameter("CustomerId", customer.CustomerId));
            dbContext.Database.ExecuteSqlCommand("DELETE FROM CreditCard WHERE CustomerId = @CustomerId", new SqlParameter("CustomerId", customer.CustomerId));

            foreach (var address in customer.Addresses)
                dbContext.Entry<CustomerAddress>(address).State = EntityState.Added;
            foreach (var card in customer.CreditCards)
                dbContext.Entry<CreditCard>(card).State = EntityState.Added;

            dbContext.Entry<Customer>(customer).State = EntityState.Modified;
        }
        else
        {
            dbContext.Entry<Customer>(customer).State = EntityState.Added;
            foreach (var card in customer.CreditCards)
                dbContext.Entry<CreditCard>(card).State = EntityState.Added;
            foreach (var address in customer.Addresses)
                dbContext.Entry<CustomerAddress>(address).State = EntityState.Added;
        }
    }

    public void Delete(int customerId)
    {
        var existingCustomer = dbContext.Customers.Find(customerId);

        if (existingCustomer != null)
        {
            //delete cards
            var creditCards = dbContext.CreditCards.Where(c => c.CustomerId == customerId);
            foreach (var card in creditCards)
                dbContext.Entry<CreditCard>(card).State = EntityState.Deleted;

            //delete addresses
            var addresses = dbContext.CustomerAddresses.Where(c => c.CustomerId == customerId);
            foreach (var address in addresses)
                dbContext.Entry<CustomerAddress>(address).State = EntityState.Deleted;

            //delete basket
            dbContext.Entry<Customer>(existingCustomer).State = EntityState.Deleted;
        }
    }

    public Customer GetById(int customerId)
    {
        return dbContext.Customers.Include("Addresses").AsNoTracking().SingleOrDefault(c => c.CustomerId == customerId);
    }

    public IList<Customer> GetPagedAndSorted(int pageNumber, int pageSize, string sortBy, SortDirection sortDirection)
    {
        return null;
    }

    public void Save()
    {
        dbContext.SaveChanges();
    }

    #endregion CoreMethods


    #region AdditionalMethods

    #endregion AdditionalMethods

}

} }

Response to b: When you create your database, you must either cascade deletes (that is the database removes all related child records, too) or have the foreign key nullable.对 b 的响应:当您创建数据库时,您必须要么级联删除(即数据库也删除所有相关的子记录)或使外键可以为空。 Then you won't get that error.那么你就不会得到那个错误。 This isn't to blame on EF, it's the way how an relational database handles constraints.这不应该归咎于 EF,而是关系数据库处理约束的方式。 You can configure this in your EDMX, your code first or using DDL on the database side.你可以在你的 EDMX 中配置它,你的代码首先或者在数据库端使用 DDL。 Depending on your decision how you did set up your project.取决于您如何设置项目的决定。

Response to c: more a general feeling, but deleting all children and reinserting sounds quite error prone and has a 'smell'.对 c 的回应:更一般的感觉,但删除所有子项并重新插入听起来很容易出错并且有“气味”。 At least I would do that only if it is absolutely required.至少只有在绝对需要时我才会这样做。 From a performance point of view, updating is probably faster.从性能的角度来看,更新可能更快。 Maybe you can rethink the problem why you chose to delete and reinsert?也许您可以重新思考为什么选择删除和​​重新插入的问题?

ok i think that i ve had enough of this for now so i ll summarize my rather negative experience好的,我想我现在已经受够了,所以我将总结我相当消极的经历

a) It is kind of possible but since this is version 5 i expected something better. a)这是可能的,但由于这是第 5 版,我希望会有更好的结果。 probably the easiest and simpler workaround can be found here http://edo-van-asseldonk.blogspot.co.uk/2012/03/readonly-collections-with-entity.html or i suppose you can even come up with your own readonly collection specific to the issue at hand such as BasketProductsReadOnlyCollection if you have a basket and a collection of its products.可能最简单和更简单的解决方法可以在这里找到http://edo-van-asseldonk.blogspot.co.uk/2012/03/readonly-collections-with-entity.html或者我想你甚至可以想出你自己的readonly 特定于手头问题的集合,例如 BasketProductsReadOnlyCollection 如果您有一个篮子及其产品的集合。

b) Probably we do not have to worry about a anyway. b) 可能我们无论如何都不必担心 a。 In a "stroke of genius" microsoft made it pretty much impossible to write proper DDD code given the problem here.鉴于这里的问题,微软在“天才之举”中几乎不可能编写正确的 DDD 代码。 If you have a Basket and Products with a BasketId in your Products table that is not nullable then you are in trouble if you do Basket.RemoveProduct(product).如果您的 Products 表中有一个 Basket 和 Products 的 BasketId 不可为空,那么如果您执行 Basket.RemoveProduct(product),就会遇到麻烦。 Removing something like this means that the "relationship" is removed not the record.删除这样的内容意味着删除的是“关系”而不是记录。 So EF will try to set BasketId to null and if it cant it ll throw an exception (and no i dont want to make it nullable just to suit EF, even if i wanted what if i work with a DBA who doesnt?) what you need to do is call dbContext.Products.Remove(product) to make sure that it is deleted.所以 EF 会尝试将 BasketId 设置为 null,如果它不能,它会抛出一个异常(不,我不想让它可以为 null 只是为了适合 EF,即使我想要如果我和一个不这样做的 DBA 一起工作怎么办?)需要做的是调用 dbContext.Products.Remove(product) 以确保它被删除。 That basically means that your business logic code needs to be aware of dbContext这基本上意味着您的业务逻辑代码需要了解 dbContext

c) I cant be bothered any more! c) 我不能再被打扰了! Again there are responses about this on StackOverflow and you can possibly get something up and running but it should not be that difficult and counter intuitive.再次在 StackOverflow 上有关于此的回应,您可能会启动并运行一些东西,但它不应该那么困难和违反直觉。

As for the bigger picture, i had a look at the N-Tier recommendations that work with "Detached "Entities.至于更大的图景,我查看了适用于“分离”实体的 N 层建议。 I read a book from Julia Lerman who seems to be the authority on the subject and im not impressed.我读了朱莉娅·勒曼 (Julia Lerman) 的一本书,她似乎是该主题的权威,但我没有留下深刻的印象。 The way the whole attaching an object graph works and the recommended ways of handling this are again very counter intuitive.整个附加对象图的工作方式以及推荐的处理方式再次非常反直觉。 Her recommended approach to make things "simple" was to have each object record its state in your business code!她推荐的使事情变得“简单”的方法是让每个对象在您的业务代码中记录其状态! not my cup of tea.不适合我。

I dont consider myself an architectural genius or something and perhaps im missing something (or a lot) but to me EF's efforts seem to be misplaced.我不认为自己是建筑天才或其他什么东西,也许我错过了一些东西(或很多),但对我来说,EF 的努力似乎放错了地方。 They spent so much time and money implementing this whole tracking system that is supposed to do everything for you (typical MS, they think we are too stupid or something to look after our own stuff) instead of focusing on other things that could make this prouduct a lot easier to use.他们花了很多时间和金钱来实施这个应该为你做所有事情的整个跟踪系统(典型的 MS,他们认为我们太愚蠢或无法照顾自己的东西),而不是专注于其他可以使这个产品成为产品的事情更容易使用。

What i want from my ORM is to deliver the data for me in my objects, then LEAVE ME ALONE to process them in whatever way i want and then i want to pass my object or object graph back to the ORM and have the freedom to tell it what i want to add/delete/update from the object graph and how without the current shenanigans of EF.我想从我的 ORM 中为我提供对象中的数据,然后让我一个人以我想要的任何方式处理它们,然后我想将我的对象或对象图传递回 ORM 并有自由告诉它是我想从对象图中添加/删除/更新的内容,以及如何在没有 EF 的当前恶作剧的情况下。

Bottom line: i think i ll give MS a couple more years on this, they ll probably get it right in the end but this is not for me yet.底线:我想我会再给 MS 几年时间,他们最终可能会做对,但这还不适合我。 And will MS finally put some proper documentation/tutorials on their sites? MS 最终会在他们的网站上放一些适当的文档/教程吗? I remember reading a 300 hundred pages PDF tutorial on NHibernate years ago.我记得几年前读过一篇关于 NHibernate 的 30000 页 PDF 教程。

a) What are you trying to do in the first place? a) 你一开始想做什么? Can't you make the collection private and expose only public property that takes a snapshot of it?您不能将集合设为私有并仅公开对其进行快照的公共属性吗?

b) To remove a child entity from the database, use dbcontext.ThatEntitySet.Remove(child) , not parent.Children.Remove(child) . b) 要从数据库中删除子实体,请使用dbcontext.ThatEntitySet.Remove(child) ,而不是parent.Children.Remove(child)

Or you can make an identifying relationship by making a foreign key in the child a part of the primary key.或者,您可以通过将子项中的外键作为主键的一部分来建立识别关系。 Then parent.Children.Remove(child) would remove a row from DB.然后parent.Children.Remove(child)将从数据库中删除一行。

c) Seems that you're doing something stupid. c) 看起来你在做一些愚蠢的事情。 If you provided details I would propose a different solution.如果您提供详细信息,我会提出不同的解决方案。

Big topic: Is your domain complex enough?大话题:您的域是否足够复杂? Or you're just trying to apply... to force DDD patterns in a simple CRUD application?或者您只是想应用……在简单的 CRUD 应用程序中强制使用 DDD 模式? What business rules do you have?你有什么业务规则? Invariants?不变量? What methods do your entities have?您的实体有哪些方法? Are there any policies?有什么政策吗?

Why would you ever need an InsertOrUpdate method?为什么需要 InsertOrUpdate 方法? I suppose that you invented it because you use just the same form for creating and updating an entity.我想您发明了它是因为您使用相同的表单来创建和更新实体。 That is a strong signal, that you're just doing a CRUD app.这是一个强烈的信号,你只是在做一个 CRUD 应用程序。

In case anyone else is struggling with this, this is the best implementation i could come up with, look at the RemoveFromBasket, AddToBasket methods, not ideal but at least you get something up & running万一其他人为此苦苦挣扎,这是我能想到的最佳实现,看看 RemoveFromBasket、AddToBasket 方法,虽然不理想,但至少你得到了一些东西并正在运行

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Web;
 using System.Web.Helpers;
 using EShop.Models.DomainModel;
 using System.Data;
 using EShop.Models.DataAccess;
 using System.Data.Objects;
 using System.Data.Entity.Infrastructure;

namespace EShop.Models.Repositories
{
public class BasketRepository : BaseRepository, IRepository<Basket, Int32>
{
    public BasketRepository() : base(new EShopData()) { }

    #region CoreMethods

    public void InsertOrUpdate(Basket basket)
    {
        var basketInDB = dbContext.Baskets.SingleOrDefault(b => b.BasketId == basket.BasketId);
        if (basketInDB == null)
            dbContext.Baskets.Add(basket);
    }

    public void Delete(int basketId)
    {
        var basket = this.GetById(basketId);
        if (basket != null)
        {
            foreach (var product in basket.BasketProducts.ToList())
            {
                basket.BasketProducts.Remove(product); //delete relationship
                dbContext.BasketProducts.Remove(product); //delete from DB
            }
            dbContext.Baskets.Remove(basket);
        }
    }

    public Basket GetById(int basketId)
    {
        // eager-load product info
        var basket = dbContext.Baskets.Include("BasketProducts")
                                      .Include("BasketProducts.Product.Brand").SingleOrDefault(b => b.BasketId == basketId);
        return basket;
    }

    public IList<Basket> GetPagedAndSorted(int pageNumber, int pageSize, string sortBy, SortDirection sortDirection)
    {
        throw new NotImplementedException();
    }

    public void Save()
    {
        dbContext.SaveChanges();
    }

    #endregion CoreMethods


    #region AdditionalMethods
    public void AddToBasket(Basket basket, Product product, int quantity)
    {
        var existingProductInBasket = dbContext.BasketProducts.Find(basket.BasketId, product.ProductId);
        if (existingProductInBasket == null)
        {
            var basketProduct = new BasketProduct()
            {
                BasketId = basket.BasketId,
                ProductId = product.ProductId,
                Quantity = quantity
            };
            basket.BasketProducts.Add(basketProduct);   
        }
        else
        {
            existingProductInBasket.Quantity = quantity;
        }
    }

    public void RemoveFromBasket(Basket basket, Product product)
    {
        var existingProductInBasket = dbContext.BasketProducts.Find(basket.BasketId, product.ProductId);
        if (existingProductInBasket != null)
        {
            basket.BasketProducts.Remove(existingProductInBasket); //delete relationship
            dbContext.BasketProducts.Remove(existingProductInBasket); //delete from DB
        }
    }

    public void RemoveFromBasket(BasketProduct basketProduct)
    {
        var basket = dbContext.Baskets.Find(basketProduct.BasketId);
        var existingProductInBasket = dbContext.BasketProducts.Find(basketProduct.BasketId, basketProduct.ProductId);
        if (basket != null && existingProductInBasket != null)
        {
            basket.BasketProducts.Remove(existingProductInBasket); //delete relationship
            dbContext.BasketProducts.Remove(existingProductInBasket); //delete from DB
        }
    }

    public void ClearBasket(Basket basket)
    {
        foreach (var product in basket.BasketProducts.ToList())
            basket.BasketProducts.Remove(product);
    }

    #endregion AdditionalMethods

}

} }

Ok, looks like i ve managed to get everything working with EF 5 more or less the way i want them.好的,看起来我已经设法让 EF 5 或多或少按照我想要的方式工作。 Problem b seems to be ok with EF5.问题 b 似乎对 EF5 没问题。 I think that i now have a proper DDD basket class and a proper repository so quite happy with that, perhaps i wan unfair being too harsh with EF after all!我认为我现在有一个合适的 DDD 篮子类和一个合适的存储库,对此非常满意,也许我毕竟对 EF 过于苛刻是不公平的!

public partial class Basket
{
    public Basket()
    {
        this.BasketProducts = new List<BasketProduct>();
    }

    public int BasketId { get; set; }
    public int? CustomerId { get; set; }
    public decimal TotalValue { get; set; }
    public DateTime Created { get; set; }
    public DateTime Modified { get; set; }

    public ICollection<BasketProduct> BasketProducts { get; private set; }

    public void AddToBasket(Product product, int quantity)
    {
        //BUSINESS LOGIC HERE
        var productInBasket = BasketProducts.SingleOrDefault(b => b.BasketId == this.BasketId &&  b.ProductId == product.ProductId);
        if (productInBasket == null)
        {
            BasketProducts.Add(new BasketProduct
            {
                BasketId = this.BasketId,
                ProductId = product.ProductId,
                Quantity = quantity
            });
        }
        else
        {
            productInBasket.Quantity = quantity;
        }
    }

    public void RemoveFromBasket(Product product)
    {
        //BUSINESS LOGIC HERE
        var prodToRemove = BasketProducts.SingleOrDefault(b => b.BasketId == this.BasketId && b.ProductId == product.ProductId);
        BasketProducts.Remove(prodToRemove);
    }
}

} }

public class BasketRepository : BaseRepository, IRepository<Basket, Int32>
{
    public BasketRepository() : base(new EShopData()) { }

    #region CoreMethods
    //public void InsertOrUpdate(Basket basket, bool persistNow = true) { }

    public void Save(Basket basket, bool persistNow = true)
    {
        var basketInDB = dbContext.Baskets.SingleOrDefault(b => b.BasketId == basket.BasketId);
        if (basketInDB == null)
            dbContext.Baskets.Add(basket);

        if (persistNow)
            dbContext.SaveChanges();
    }

    public void Delete(int basketId, bool persistNow = true)
    {
        var basket = this.GetById(basketId);
        if (basket != null)
        {
            foreach (var product in basket.BasketProducts.ToList())
            {
                basket.BasketProducts.Remove(product); //delete relationship
                dbContext.BasketProducts.Remove(product); //delete from DB
            }
            dbContext.Baskets.Remove(basket);
        }
        if (persistNow)
            dbContext.SaveChanges();
    }

    public Basket GetById(int basketId)
    {
        // eager-load product info
        var basket = dbContext.Baskets.Include("BasketProducts")
                                      .Include("BasketProducts.Product.Brand").SingleOrDefault(b => b.BasketId == basketId);
        return basket;
    }

    public IList<Basket> GetPagedAndSorted(int pageNumber, int pageSize, string sortBy, SortDirection sortDirection)
    {
        throw new NotImplementedException();
    }

    public void SaveForUnitOfWork()
    {
        dbContext.SaveChanges();
    }

} }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM