EntityFramework加载/更新实体

时间:2019-02-10 16:44:25

标签: c# entity-framework entity-framework-6

我现在正在努力了解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();

现在我正尝试仅加载来自当前用户的待办事项,因此我尝试了此操作:

Context.dbsCategories.Where(c => c.Id == id).Include(c => c.Todos.Where(t => t.Owner == Settings.User).Select(t => t.Infos)).FirstOrDefault();

这行不通,因为无法在include内进行过滤,因此我尝试了以下操作:

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在这里到底在做什么?

现在是我的主要问题->数据库和客户端之间的数据同步。我使用的是运行时间很长的Context,只要应用程序正在运行,它就会一直存在,以将对拥有项目所做的更改保存到数据库中。用户无法操作/删除其他用户的数据,这是用户界面所保证的。 为了同步数据,我构建了此同步方法,该方法每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 个答案:

答案 0 :(得分:0)

我,您确实想偏离entity framework code-first conventions,对吗?

(1)不正确的类定义

表之间的关系是列表,而不是ICollection,它们不是声明为虚拟的,而您却忘记了声明外键

Todo与Category之间存在一对多的关系:每个Todo恰好属于一个Category(使用外键),每个Category都有零个或多个Todo。

您选择为类别赋予属性List<Todo> Todos {get; set;}。您确定category.Todos[4]具有定义的含义吗?那么,category.Todos.Insert(4,new Todo())意味着什么?

最好坚持使用一个接口,在该接口上不能使用数据库中没有适当含义的函数:使用`ICollection Todos {get;组;}。这样,您将只能访问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 Category has zero or more Infos:
    public virtual ICollection<Info> Infos { get; set; }
}

您现在可能会猜到信息:

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; }
}

三个主要改进:ICollection,虚拟,外键定义:它们是表中的真实列。

(2)使用“选择”代替“包含”

数据库查询的较慢部分之一是将所选数据从数据库管理系统传输到本地进程。因此,限制传输的数据量是明智的。

假设ID为4的类别有1000个待办事项。每个Todo都有一个属于它的Category的外键,其值为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存活来对其进行优化。即使您查询大量数据,显示的海量数据也将成为瓶颈,而不是数据库查询。

最好一次获取数据,处置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的用户将不会注意到它。

请考虑为从数据库中获取的数据创建一个特殊的命名空间。这样,您可以将获取的类别仍称为类别,而不是RepositoryCategory。您甚至可以更好地隐藏从中获取数据的位置。

回到您的问题

您写道:

  

现在我正尝试仅加载当前用户的待办事项

经过先前的改进,这很容易:

string owner = Settings.User; // or something similar
var result = dbContext.Todos.Where(todo => todo.Owner == owner)
    .Select(todo => new 
    {
         // properties you need
    })