c# - EntityFramework 加载/更新实体
问题描述
我现在正在努力了解 EF 如何加载/更新实体。首先,我想解释一下我的应用程序(WPF)是关于什么的。我正在开发一个应用程序,用户可以在其中将待办事项存储在类别中,这些类别是由应用程序预定义的。每个用户都可以阅读所有项目,但只能删除/更新他自己的项目。它是一个多用户系统,意味着应用程序在网络中运行多次访问同一个 sql server 数据库。当用户添加/删除/更新项目时,所有其他正在运行的应用程序上的 UI 必须更新。
我的模型如下所示:
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public List<Todo> Todos { get; set; }
}
public class Todo
{
public int Id { get; set; }
public string Content { get; set; }
public DateTime LastUpdate { get; set; }
public string Owner { get; set; }
public Category Category { get; set; }
public List<Info> Infos { get; set; }
}
public class Info
{
public int Id { get; set; }
public string Value { get; set; }
public Todo Todo { get; set; }
}
我正在像这样进行初始负载,效果很好:
Context.dbsCategories.Where(c => c.Id == id).Include(c => c.Todos.Select(t => t.Infos)).FirstOrDefault();
现在我试图只加载来自当前用户的 Todos,因此我尝试了这个:
Context.dbsCategories.Where(c => c.Id == id).Include(c => c.Todos.Where(t => t.Owner == Settings.User).Select(t => t.Infos)).FirstOrDefault();
这不起作用,因为无法在包含中进行过滤,所以我尝试了这个:
var cat = Context.dbsCategories.Where(c => c.Id == id).FirstOrDefault();
Context.dbsTodos.Where(t => t.Category.Id == cat.Id && t.Owner == Settings.User).Include(t=>t.Infos);
在执行第二行查找 Todo 项目后,这些项目会自动添加到 cat 的 Todos 集合中。为什么?我本来希望我必须手动将它们添加到 cat 的 Todos 集合中。只是为了让我了解 EF 到底在做什么?
现在到我的主要问题-> 数据库和客户端之间的数据同步。我正在使用一个长时间运行的上下文,只要应用程序正在运行,它就可以保存对拥有项目所做的数据库的更改。用户无法操作/删除其他用户的数据,这是由用户界面保证的。为了同步数据,我构建了这个同步方法,它将每 10 秒运行一次,现在它是手动触发的。
这就是我的同步代码,它只将不属于它的项目同步到客户端。
private async Task Synchronize()
{
using (var ctx = new Context())
{
var database = ctx.dbsTodos().Where(x => x.Owner != Settings.User).Select(t => t.Infos).AsNoTracking();
var loaded = Context.dbsTodos.Local.Where(x => x.Owner != Settings.User);
//In local context but not in database anymore -> Detachen
foreach (var detach in loaded.Except(database, new TodoIdComparer()).ToList())
{
Context.ObjectContext.Detach(detach);
Log.Debug(this, $"Item {detach} detached");
}
//In database and local context -> Check Timestamp -> Update
foreach (var update in loaded.Intersect(database, new TodoIdTimeStampComparer()))
{
await Context.Entry(update).ReloadAsync();
Log.Debug(this, $"Item {update} updated");
}
//In database but not in local context -> Attach
foreach (var attach in database.ToList().Except(loaded, new TodoIdComparer()))
{
Context.dbsTodos().Attach(attach);
Log.Debug(this, $"Item {attach} attached");
}
}
}
我遇到以下问题/未知来源的问题:分离已删除的项目似乎有效,现在我不确定是仅分离了待办事项还是信息。
更新项目仅适用于 TodoItem 本身,而不是重新加载其中的信息?我怎样才能重新加载整个实体的所有关系?我很感谢这方面的每一个帮助,即使你说我在这里做的都是错的!
到目前为止,附加新项目和信息不起作用?我在这里做错了什么?
这是在客户端和数据库之间同步数据的正确方法吗?我在这里做错了什么?是否有任何“如何同步”教程?到目前为止我还没有发现任何有用的东西?
谢谢!
解决方案
我的,你确实喜欢偏离实体框架代码优先的约定,是吗?
(1) 错误的类定义
您的表之间的关系是列表,而不是 ICollections,它们未声明为虚拟的,您忘记声明外键
Todo 和 Category 之间存在一对多的关系:每个都Todo
恰好属于一个Category
(使用外键),每个 Category 都有零个或多个 Todo。
您选择给 Category 一个属性:
List<Todo> Todos {get; set;}
- 你确定它
category.Todos[4]
有明确的含义吗? - 什么
category.Todos.Insert(4, new Todo())
意思?
最好坚持一个界面,您不能使用在数据库中没有适当含义的函数:使用ICollection<Todo> Todos {get; set;}
. 这样,您将只能访问 Entity Framework 可以转换为 SQL 的函数。
此外,查询可能会更快:您为实体框架提供了以最有效的方式查询数据的可能性,而不是强制将结果放入列表中。
在实体框架中,表的列由非虚拟属性表示;虚拟属性表示表之间的关系(一对多,多对多)
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
... // other properties
// every Category has zero or more Todos (one-to-many)
public virtual ICollection<Todo> Todos { get; set; }
}
public class Todo
{
public int Id { get; set; }
public string Content { get; set; }
... // other properties
// every Todo belongs to exactly one Category, using foreign key
public int CategoryId { get; set }
public virtual Category Category { get; set; }
// every Todo has zero or more Infos:
public virtual ICollection<Info> Infos { get; set; }
}
你现在可能已经猜到 Info 了:
public class Info
{
public int Id { get; set; }
public string Value { get; set; }
... // other properties
// every info belongs to exactly one Todo, using foreign key
public int TodoId {get; set;}
public virtual Todo Todo { get; set; }
}
三大改进:
- ICollections 而不是列表
- ICollections 是虚拟的,因为它不是表中的真实列,
- 外键定义非虚拟:它们是表中的真实列。
(2) 使用 Select 而不是 Include
数据库查询中较慢的部分之一是将所选数据从数据库管理系统传输到本地进程。因此,明智的做法是限制传输的数据量。
假设 ID 为 [4] 的类别有一千个待办事项。此类别的每个 Todo 都会有一个值为 4 的外键。因此,相同的值 4 将被传输 1001 次。多么浪费处理能力!
在实体框架中使用 Select 而不是 Include 来查询数据并仅选择您实际计划使用的属性。仅当您计划更新选定数据时才使用包含。
给我所有...的类别以及他们的待办事项...
var results = dbContext.Categories
.Where(category => ...)
.Select(category => new
{
// only select properties that you plan to use
Id = category.Id,
Name = category.Name,
...
Todos = category.Todos
.Where(todo => ...) // only if you don't want all Todos
.Select(todo => new
{
// again, select only the properties you'll plan to use
Id = todo.Id,
...
// not needed, you know the value:
// CategoryId = todo.CategoryId,
// only if you also want some infos:
Infos = todo.Infos
.Select(info => ....) // you know the drill by now
.ToList(),
})
.ToList(),
});
(3) 不要让 DbContext 存活这么久!
另一个问题是你保持DbContext
开放很长一段时间。这不是 dbContext 的意思。如果您的数据库在查询和更新之间发生变化,您将遇到麻烦。我很难想象您查询了如此多的数据,以至于您需要通过保持 dbContext 活动来对其进行优化。即使你查询了很多数据,这个海量数据的展示将是瓶颈,而不是数据库查询。
最好一次获取数据,dispose DbContext,在再次更新获取数据时,更新更改的属性和SaveChanges。
获取数据:
RepositoryCategory FetchCategory(int categoryId)
{
using (var dbContext = new MyDbContext())
{
return dbContext.Categories.Where(category => category.Id == categoryId)
.Select(category => new RepositoryCategory
{
... // see above
})
.FirstOrDefault();
}
}
是的,你需要一个额外的课程RepositoryCategory
。优点是,您隐藏了从数据库中获取数据的信息。如果您从 CSV 文件或互联网获取数据,您的代码几乎不会改变。这样可以更好地测试,也可以更好地维护:如果数据库中的 Category 表发生变化,RepositoryCategory 的用户不会注意到它。
考虑为从数据库中获取的数据创建一个特殊的命名空间。这样,您可以将获取的 Category 命名为 Category,而不是 RepositoryCategory。您甚至可以更好地隐藏从哪里获取数据。
回到你的问题
你写了:
现在我试图只加载来自当前用户的 Todos
在之前的改进之后,这将很容易:
string owner = Settings.User; // or something similar
var result = dbContext.Todos.Where(todo => todo.Owner == owner)
.Select(todo => new
{
// properties you need
})