可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
With EF
a) You cannot have proper readonly collections
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
c) There is no easy way of deleting all child items of a parent and reinserting them
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? 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. That way i get my info and EF leaves alone without doing
whatever the hell it does behind my back. I can now Remove from a collection and i can kind of
be able to delete as well (who would think that i d have to go back to sql fro this!)
Does anyone know any pitfalls of using AsNoTracking? As far as i can genearate SQL based
on my objects and populate them or update/delete them i am fine. 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
}
}
回答1:
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. Then you won't get that error. This isn't to blame on EF, it's the way how an relational database handles constraints. You can configure this in your EDMX, your code first or using DDL on the database side. 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'. 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?
回答2:
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.
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.
b) Probably we do not have to worry about a anyway. In a "stroke of genius" microsoft made it pretty
much impossible to write proper DDD code given the problem here. 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). 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. That basically means that your business logic code needs to be aware of dbContext
c) I cant be bothered any more! 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.
As for the bigger picture, i had a look at the N-Tier recommendations that work with "Detached "Entities. I read a book from Julia Lerman who seems to be the authority on the subject and i m not impressed. 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 i m missing something (or a lot) but to me EF's efforts seem to be misplaced. 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.
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.
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. And will MS finally put some proper documentation/tutorials on their sites? I remember reading a 300 hundred pages PDF tutorial on NHibernate years ago.
回答3:
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
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
}
}
回答4:
Ok, looks like i ve managed to get everything working with EF 5 more or less the way i want them.
Problem b seems to be ok with 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!
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();
}
}
回答5:
a) What are you trying to do in the first place? 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)
.
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.
c) Seems that you're doing something stupid. 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? 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? 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.