代码优先 Entity Framework。急于加载,验证然后保存导致错误
Code First Entity Framework. Eager loading, validating then saving causing error
我的设置是我有一个包含物品的篮子。项目由产品和尺寸组成。产品与尺寸存在多对多关系,因此我可以验证给定尺寸是否对给定产品有效。我希望能够将一个项目添加到购物车中,执行一些验证并保存到数据库中。
我创建了一个演示程序来演示我遇到的问题。当程序运行时,已经有一个篮子保存到数据库中(参见 DBInitializer)。它有一个很大的东西。在程序中你可以看到我加载了篮子,加载了一个小尺寸和一个条形产品。我将大条添加到篮子中。篮子做了一些内部验证,我保存到数据库中。这可以正常工作。
当我尝试添加数据库中已存在的不同大小的产品时,问题就来了。因此,如果我们尝试向篮子中添加一个大柱并保存,我们将得到一个空引用异常。这不是我想要的行为,因为包含 2 个项目(一个大 foo 和一个小 foo)的篮子是完全有效的。
我很确定问题出在我们已经通过预先加载将 foo 加载到篮子中这一事实。我试过注释掉篮子项目的急切加载,这很有效。但是,如果可能的话,我想要一个能够保持急切加载的解决方案。
注意:我在 dbcontext class 中添加了一个额外的方法,即 int SaveChanges(bool excludeReferenceData)。这会停止将额外的产品和尺寸记录保存回数据库。我已经制作了我所有的构造函数、getter 和 setter public 以便更容易地复制我的问题。我的演示代码是在针对 .net framework 4.5.2 的控制台应用程序上创建的。 Entity framework的版本是6.2。
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Linq;
using static Demo.Constants;
namespace Demo
{
public static class Constants
{
public static int BasketId => 1;
public static int SmallId => 1;
public static int LargeId => 2;
public static int FooId => 1;
public static int BarId => 2;
}
public class Program
{
public static void Main()
{
using (var context = new AppContext())
{
var customerBasket = context.Baskets
.Include(b => b.Items.Select(cbi => cbi.Product))
.Include(b => b.Items.Select(cbi => cbi.Size))
.SingleOrDefault(b => b.Id == BasketId);
var size = context.Sizes.AsNoTracking()
.SingleOrDefault(s => s.Id == SmallId);
context.Configuration.ProxyCreationEnabled = false;
var product = context
.Products
.AsNoTracking()
.Include(p => p.Sizes)
.SingleOrDefault(p => p.Id == BarId);
//changing BarId to FooId in the above line results in
//null reference exception when savechanges is called.
customerBasket.AddItem(product, size);
context.SaveChanges(excludeReferenceData: true);
}
Console.ReadLine();
}
}
public class Basket
{
public int Id { get; set; }
public virtual ICollection<Item> Items { get; set; }
public Basket()
{
Items = new Collection<Item>();
}
public void AddItem(Product product, Size size)
{
if (itemAlreadyExists(product, size))
{
throw new InvalidOperationException("item already in basket");
}
var newBasketItem = Item.Create(
this,
product,
size);
Items.Add(newBasketItem);
}
private bool itemAlreadyExists(Product product, Size size)
{
return Items.Any(a => a.ProductId == product.Id && a.SizeId == size.Id);
}
}
public class Item
{
public Guid Id { get; set; }
public int BasketId { get; set; }
public virtual Product Product { get; set; }
public int ProductId { get; set; }
public virtual Size Size { get; set; }
public int SizeId { get; set; }
public Item()
{
}
public string getDescription()
{
return $"{Product.Name} - {Size.Name}";
}
internal static Item Create(Basket basket
, Product product,
Size size)
{
Guid id = Guid.NewGuid();
if (!product.HasSize(size))
{
throw new InvalidOperationException("product does not come in size");
}
var basketItem = new Item
{
Id = id,
BasketId = basket.Id,
Product = product,
ProductId = product.Id,
Size = size,
SizeId = size.Id
};
return basketItem;
}
}
public class Product : IReferenceObject
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<ProductSize> Sizes { get; set; }
public Product()
{
Sizes = new Collection<ProductSize>();
}
public bool HasSize(Size size)
{
return Sizes.Any(s => s.SizeId == size.Id);
}
}
public class ProductSize : IReferenceObject
{
public int SizeId { get; set; }
public virtual Size Size { get; set; }
public int ProductId { get; set; }
}
public class Size : IReferenceObject
{
public int Id { get; set; }
public string Name { get; set; }
}
public class AppContext : DbContext
{
public DbSet<Basket> Baskets { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<Size> Sizes { get; set; }
public AppContext()
: base("name=DefaultConnection")
{
Database.SetInitializer(new DBInitializer());
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Basket>()
.HasMany(c => c.Items)
.WithRequired()
.HasForeignKey(c => c.BasketId)
.WillCascadeOnDelete(true);
modelBuilder.Entity<Item>()
.Property(c => c.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
modelBuilder.Entity<Item>()
.HasKey(c => new { c.Id, c.BasketId });
modelBuilder.Entity<ProductSize>()
.HasKey(c => new { c.ProductId, c.SizeId });
base.OnModelCreating(modelBuilder);
}
public int SaveChanges(bool excludeReferenceData)
{
if(excludeReferenceData)
{
var referenceEntries =
ChangeTracker.Entries<IReferenceObject>()
.Where(e => e.State != EntityState.Unchanged
&& e.State != EntityState.Detached);
foreach (var entry in referenceEntries)
{
entry.State = EntityState.Detached;
}
}
return SaveChanges();
}
}
public interface IReferenceObject
{
}
public class DBInitializer: DropCreateDatabaseAlways<AppContext>
{
protected override void Seed(AppContext context)
{
context.Sizes.Add(new Size { Id = LargeId, Name = "Large" });
context.Sizes.Add(new Size { Id = SmallId, Name = "Small" });
context.Products.Add(
new Product
{
Id = FooId,
Name = "Foo",
Sizes = new Collection<ProductSize>()
{
new ProductSize{ProductId = FooId, SizeId = LargeId},
new ProductSize{ProductId = FooId, SizeId =SmallId}
}
});
context.Products.Add(new Product { Id = BarId, Name = "Bar",
Sizes = new Collection<ProductSize>()
{
new ProductSize{ProductId = BarId, SizeId = SmallId}
}
});
context.Baskets.Add(new Basket
{
Id = BasketId,
Items = new Collection<Item>()
{
new Item
{
Id = Guid.NewGuid(),
BasketId =BasketId,
ProductId = FooId,
SizeId = LargeId
}
}
});
base.Seed(context);
}
}
}
当您使用 AsNoTracking 时,这会告诉 EF 不包括正在加载到 DbContext ChangeTracker 中的对象。当您加载要返回的数据并且知道此时您不想将其保存回来时,您通常希望这样做。因此,我认为您只需要摆脱所有调用的 AsNoTracking,它应该可以正常工作。
我的设置是我有一个包含物品的篮子。项目由产品和尺寸组成。产品与尺寸存在多对多关系,因此我可以验证给定尺寸是否对给定产品有效。我希望能够将一个项目添加到购物车中,执行一些验证并保存到数据库中。
我创建了一个演示程序来演示我遇到的问题。当程序运行时,已经有一个篮子保存到数据库中(参见 DBInitializer)。它有一个很大的东西。在程序中你可以看到我加载了篮子,加载了一个小尺寸和一个条形产品。我将大条添加到篮子中。篮子做了一些内部验证,我保存到数据库中。这可以正常工作。
当我尝试添加数据库中已存在的不同大小的产品时,问题就来了。因此,如果我们尝试向篮子中添加一个大柱并保存,我们将得到一个空引用异常。这不是我想要的行为,因为包含 2 个项目(一个大 foo 和一个小 foo)的篮子是完全有效的。
我很确定问题出在我们已经通过预先加载将 foo 加载到篮子中这一事实。我试过注释掉篮子项目的急切加载,这很有效。但是,如果可能的话,我想要一个能够保持急切加载的解决方案。
注意:我在 dbcontext class 中添加了一个额外的方法,即 int SaveChanges(bool excludeReferenceData)。这会停止将额外的产品和尺寸记录保存回数据库。我已经制作了我所有的构造函数、getter 和 setter public 以便更容易地复制我的问题。我的演示代码是在针对 .net framework 4.5.2 的控制台应用程序上创建的。 Entity framework的版本是6.2。
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Linq;
using static Demo.Constants;
namespace Demo
{
public static class Constants
{
public static int BasketId => 1;
public static int SmallId => 1;
public static int LargeId => 2;
public static int FooId => 1;
public static int BarId => 2;
}
public class Program
{
public static void Main()
{
using (var context = new AppContext())
{
var customerBasket = context.Baskets
.Include(b => b.Items.Select(cbi => cbi.Product))
.Include(b => b.Items.Select(cbi => cbi.Size))
.SingleOrDefault(b => b.Id == BasketId);
var size = context.Sizes.AsNoTracking()
.SingleOrDefault(s => s.Id == SmallId);
context.Configuration.ProxyCreationEnabled = false;
var product = context
.Products
.AsNoTracking()
.Include(p => p.Sizes)
.SingleOrDefault(p => p.Id == BarId);
//changing BarId to FooId in the above line results in
//null reference exception when savechanges is called.
customerBasket.AddItem(product, size);
context.SaveChanges(excludeReferenceData: true);
}
Console.ReadLine();
}
}
public class Basket
{
public int Id { get; set; }
public virtual ICollection<Item> Items { get; set; }
public Basket()
{
Items = new Collection<Item>();
}
public void AddItem(Product product, Size size)
{
if (itemAlreadyExists(product, size))
{
throw new InvalidOperationException("item already in basket");
}
var newBasketItem = Item.Create(
this,
product,
size);
Items.Add(newBasketItem);
}
private bool itemAlreadyExists(Product product, Size size)
{
return Items.Any(a => a.ProductId == product.Id && a.SizeId == size.Id);
}
}
public class Item
{
public Guid Id { get; set; }
public int BasketId { get; set; }
public virtual Product Product { get; set; }
public int ProductId { get; set; }
public virtual Size Size { get; set; }
public int SizeId { get; set; }
public Item()
{
}
public string getDescription()
{
return $"{Product.Name} - {Size.Name}";
}
internal static Item Create(Basket basket
, Product product,
Size size)
{
Guid id = Guid.NewGuid();
if (!product.HasSize(size))
{
throw new InvalidOperationException("product does not come in size");
}
var basketItem = new Item
{
Id = id,
BasketId = basket.Id,
Product = product,
ProductId = product.Id,
Size = size,
SizeId = size.Id
};
return basketItem;
}
}
public class Product : IReferenceObject
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<ProductSize> Sizes { get; set; }
public Product()
{
Sizes = new Collection<ProductSize>();
}
public bool HasSize(Size size)
{
return Sizes.Any(s => s.SizeId == size.Id);
}
}
public class ProductSize : IReferenceObject
{
public int SizeId { get; set; }
public virtual Size Size { get; set; }
public int ProductId { get; set; }
}
public class Size : IReferenceObject
{
public int Id { get; set; }
public string Name { get; set; }
}
public class AppContext : DbContext
{
public DbSet<Basket> Baskets { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<Size> Sizes { get; set; }
public AppContext()
: base("name=DefaultConnection")
{
Database.SetInitializer(new DBInitializer());
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Basket>()
.HasMany(c => c.Items)
.WithRequired()
.HasForeignKey(c => c.BasketId)
.WillCascadeOnDelete(true);
modelBuilder.Entity<Item>()
.Property(c => c.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
modelBuilder.Entity<Item>()
.HasKey(c => new { c.Id, c.BasketId });
modelBuilder.Entity<ProductSize>()
.HasKey(c => new { c.ProductId, c.SizeId });
base.OnModelCreating(modelBuilder);
}
public int SaveChanges(bool excludeReferenceData)
{
if(excludeReferenceData)
{
var referenceEntries =
ChangeTracker.Entries<IReferenceObject>()
.Where(e => e.State != EntityState.Unchanged
&& e.State != EntityState.Detached);
foreach (var entry in referenceEntries)
{
entry.State = EntityState.Detached;
}
}
return SaveChanges();
}
}
public interface IReferenceObject
{
}
public class DBInitializer: DropCreateDatabaseAlways<AppContext>
{
protected override void Seed(AppContext context)
{
context.Sizes.Add(new Size { Id = LargeId, Name = "Large" });
context.Sizes.Add(new Size { Id = SmallId, Name = "Small" });
context.Products.Add(
new Product
{
Id = FooId,
Name = "Foo",
Sizes = new Collection<ProductSize>()
{
new ProductSize{ProductId = FooId, SizeId = LargeId},
new ProductSize{ProductId = FooId, SizeId =SmallId}
}
});
context.Products.Add(new Product { Id = BarId, Name = "Bar",
Sizes = new Collection<ProductSize>()
{
new ProductSize{ProductId = BarId, SizeId = SmallId}
}
});
context.Baskets.Add(new Basket
{
Id = BasketId,
Items = new Collection<Item>()
{
new Item
{
Id = Guid.NewGuid(),
BasketId =BasketId,
ProductId = FooId,
SizeId = LargeId
}
}
});
base.Seed(context);
}
}
}
当您使用 AsNoTracking 时,这会告诉 EF 不包括正在加载到 DbContext ChangeTracker 中的对象。当您加载要返回的数据并且知道此时您不想将其保存回来时,您通常希望这样做。因此,我认为您只需要摆脱所有调用的 AsNoTracking,它应该可以正常工作。