当我尝试一次添加一个包含多个子节点的实体时,我在重新排序插件时遇到问题。我有一个3级结构,每个结构之间有一对多的关系(Outer 1--* Item 1--* SubItem
)。如果我尝试插入一个带有Items和Subitems的新Outer,那么包含SubItems的Items最终会被插入。
示例代码(.NET 4.5,EF 5.0.0-rc):
public class Outer
{
public int OuterId { get; set; }
public virtual IList<Item> Items { get; set; }
}
public class Item
{
public int OuterId { get; set; }
[ForeignKey("OuterId")]
public virtual Outer Outer { get; set; }
public int ItemId { get; set; }
public int Number { get; set; }
public virtual IList<SubItem> SubItems { get; set; }
}
public class SubItem
{
public int SubItemId { get; set; }
[ForeignKey("ItemId")]
public virtual Item Item { get; set; }
public int ItemId { get; set; }
}
public class MyContext : DbContext
{
public DbSet<Outer> Outers { get; set; }
public DbSet<Item> Items { get; set; }
public DbSet<SubItem> SubItems { get; set; }
}
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new DropCreateDatabaseAlways<MyContext>());
MyContext context = new MyContext();
// Add an Outer object, with 3 Items, the middle one having a subitem
Outer outer1 = new Outer { Items = new List<Item>() };
context.Outers.Add(outer1);
outer1.Items.Add(new Item { Number = 1, SubItems = new List<SubItem>() });
outer1.Items.Add(new Item { Number = 2, SubItems = new List<SubItem>(new SubItem[] { new SubItem() }) });
outer1.Items.Add(new Item { Number = 3, SubItems = new List<SubItem>() });
context.SaveChanges();
// Print the order these have ended up in
foreach (Item item in context.Items)
{
Console.WriteLine("{0}\t{1}", item.ItemId, item.Number);
}
// Produces output:
// 1 2
// 2 1
// 3 3
}
}
我知道this answer by Alex James指出插入可能需要重新排序以满足关系约束,但这不是问题。他的回答还提到他们无法跟踪保留订单的结构中的项目顺序,如列表。
我想知道的是我如何才能订购这些刀片。虽然我可以依靠通过除PK以外的字段对插入的项目进行排序,但如果我可以依赖PK顺序,它会更有效率。我真的不想使用多个SaveChanges调用来完成此任务。
我正在使用EF5 RC,但从其他未解答的问题来看,这已经有一段时间了!
答案 0 :(得分:7)
我想知道的是如何才能订购这些刀片。
你做不到。数据库命令的顺序是EF的内部行为。如果要控制命令的顺序,请不要使用从低级数据库交互中抽象出来的工具 - 直接使用SQL。
根据评论进行编辑:
是的,它是低级别的交互,因为在使用您无法控制的抽象时,您会对SQL命令的顺序产生期望。在高层次上,你会得到不同的东西,因为你正在使用那些不能用于抽象的期望。如果要控制SQL命令的顺序,则必须通过逐个保存项目来强制执行EF(=&gt;多个SaveChanges
和TransactionScope
),或者自己编写SQL。否则使用单独的列进行排序。
顺便说一下。 EF不会像您看到的那样保存实体。它有自己的更改跟踪器,包含对所有附加实例的引用。引用包含在多个Dictionary
实例和字典doesn't preserve insertion order中。如果这些集合用于生成SQL命令(我猜它们是),则无法保证订单。
答案 1 :(得分:1)
数据库中的表是集合。这意味着订单无法保证。我假设您的示例中您希望按“数字”排序结果。如果这是您想要的,如果该数字发生变化并且它不再反映数据库中的顺序,您打算做什么?
如果你真的想按特定顺序插入行,那么多个SaveChanges是你最好的选择。
没有人想多次调用SaveChanges的原因是因为它确实是这样的:一个肮脏的黑客。
由于主键是一种技术概念,因此无论如何都不应该在此键上订购结果。您可以按特定字段对结果进行排序,并为此使用数据库索引。你可能不会看到速度上的差异。
明确订购还有其他好处: 对于必须维护它的人来说,它更容易理解。否则,该人必须知道主键上的排序很重要并给出正确的结果,因为在您的应用程序的其他(完全)不相关的部分中,它意外地与数字字段的顺序相同。
答案 2 :(得分:1)
我找到了一种方法。只是以为我会让你知道:
using (var dbContextTransaction = dbContext.Database.BeginTransaction())
{
dbContext.SomeTables1.Add(object1);
dbContext.SaveChanges();
dbContext.SomeTables1.Add(object2);
dbContext.SaveChanges();
dbContextTransaction.Commit();
}
答案 3 :(得分:0)
多个Add()或保存之前的AddRange()不会保留顺序。同样,在阅读集合时,也不能保证以最初添加时的顺序返回结果。您需要向实体添加一些属性,并且在查询时使用OrderBy()来确保它们以所需的顺序返回。
答案 4 :(得分:0)
另一种方法是,通过添加实体状态更改来在添加每个条目(但在很大程度上取决于应用程序逻辑)之后不进行数据库往返操作。
在我的情况下-节点层次结构-我必须先保留根节点,然后再保留其余层次结构,以便自动路径计算起作用。
所以我有一个 root 节点,没有提供父ID,而提供了父ID的子节点。
EF Core随机地(或通过复杂和智能的逻辑-您喜欢:)随机安排节点插入,从而中断路径计算过程。
因此,我使用了覆盖上下文的 SaveChanges 方法,并检查了需要维护插入顺序的集合中的实体-首先分离个子节点,然后保存更改,并附加子节点并再次保存更改。
// select child nodes first - these entites should be added last
List<EntityEntry<NodePathEntity>> addedNonRoots = this.ChangeTracker.Entries<NodePathEntity>().Where(e => e.State == EntityState.Added && e.Entity.NodeParentId.HasValue == true).ToList();
// select root nodes second - these entities should be added first
List<EntityEntry<NodePathEntity>> addedRoots = this.ChangeTracker.Entries<NodePathEntity>().Where(e => e.State == EntityState.Added && e.Entity.NodeParentId.HasValue == false).ToList();
if (!Xkc.Utilities.IsCollectionEmptyOrNull(addedRoots))
{
if (!Xkc.Utilities.IsCollectionEmptyOrNull(addedNonRoots))
{
// detach child nodes, so they will be ignored on SaveChanges call
// no database inserts will be generated for them
addedNonRoots.ForEach(e => e.State = EntityState.Detached);
// run SaveChanges - since root nodes are still there,
// in ADDED state, inserts will be executed for these entities
int detachedRowCount = base.SaveChanges();
// re-attach child nodes to the context
addedNonRoots.ForEach(e => e.State = EntityState.Added);
// run SaveChanges second time, child nodes are saved
return base.SaveChanges() + detachedRowCount;
}
}
这种方法不能让您保留各个实体的顺序,但是如果您可以将实体归类为必须先插入的实体,然后再将其插入的实体归类-此 hack 可能会有所帮助。 / p>
答案 5 :(得分:0)
这不专业。但是,我用这种方法解决了我的问题。
PublicMenu.cs文件:
public class PublicMenu
{
public int Id { get; set; }
public int? Order { get; set; }
public int? ParentMenuId { get; set; }
[ForeignKey("ParentMenuId")]
public virtual PublicMenu Parent { get; set; }
public virtual ICollection<PublicMenu> Children { get; set; }
public string Controller { get; set; }
public string Action { get; set; }
public string PictureUrl { get; set; }
public bool Enabled { get; set; }
}
PublicMenus.json文件
[
{
"Order": 1,
"Controller": "Home",
"Action": "Index",
"PictureUrl": "",
"Enabled": true
},
{
"Order": 2,
"Controller": "Home",
"Action": "Services",
"PictureUrl": "",
"Enabled": true
},
{
"Order": 3,
"Controller": "Home",
"Action": "Portfolio",
"PictureUrl": "",
"Enabled": true
},
{
"Order": 4,
"Controller": "Home",
"Action": "About",
"PictureUrl": "",
"Enabled": true
},
{
"Order": 5,
"Controller": "Home",
"Action": "Contact",
"PictureUrl": "",
"Enabled": true
}
]
DataContextSeed文件
public class DataContextSeed
{
public static async Task SeedAsync(ICMSContext context, ILoggerFactory loggerFactory)
{
try
{
if (!context.PublicMenus.Any())
{
var publicMenusData = File.ReadAllText("../Infrastructure/Data/SeedData/PublicMenus.json");
var publicMenus = JsonSerializer.Deserialize<List<PublicMenu>>(publicMenusData);
foreach (var item in publicMenus)
{
context.PublicMenus.Add(item);
await context.SaveChangesAsync();
}
}
}
catch (Exception ex)
{
var logger = loggerFactory.CreateLogger<ICMSContextSeed>();
logger.LogError(ex.Message);
}
}
}