更新EFCore联接的通用方法 [英] Generic method for updating EFCore joins

查看:130
本文介绍了更新EFCore联接的通用方法的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我发现EFCore处理多对多关系的方式确实很乏味,那就是更新实体加入的集合。经常需要一个视图模型来自前端,并带有一个新的嵌套实体列表,而我必须为每个嵌套实体编写一个方法,该方法可以确定需要删除的内容,需要添加的内容,然后执行删除和添加操作。有时一个实体具有多个多对多的关系,我必须为每个集合写几乎相同的代码。

One thing that I am finding really tedious with the way EFCore handles many-to-many relationships is updating an entities joined collections. It is a frequent requirement that a viewmodel comes from frontend with a new list of nested entities and I have to write a method for each nested entity that works out what needs to be removed, what needs to be added and then do the removes and adds. Sometimes an entity has multiple many-to-many relationships and I have to write out pretty much the same code for each collection.

我认为这里可以使用一种通用方法来阻止我重复自己,但我正在努力寻找方法。

I think a generic method could be used here to stop me repeating myself but I am struggling to work out how.

首先让我向您展示我目前的操作方式。

First let me show you way I currently do it.

可以说我们有以下模型:

Lets say we have these models:

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }

    public virtual ICollection<PersonCar> PersonCars { get; set; } = new List<PersonCar>();
}

public class Car
{
    public int Id { get; set; }
    public string Manufacturer { get; set; }

    public virtual ICollection<PersonCar> PersonCars { get; set; } = new List<PersonCar>();
}

public class PersonCar
{
    public virtual Person Person { get; set; }
    public int PersonId { get; set; }
    public virtual Car Car { get; set; }
    public int CarId { get; set; }
}

以及使用流畅的API定义的键

And a key defined with fluent API

modelBuilder.Entity<PersonCar>().HasKey(t => new { t.PersonId, t.CarId });

然后我们添加一个新的Person和关联的汽车列表:

And we add a new Person and list of associated cars:

var person = new Person
{
    Name = "John",
    PersonCars = new List<PersonCar>
    {
        new PersonCar { CarId = 1 },
        new PersonCar { CarId = 2 },
        new PersonCar { CarId = 3 }
    }
};

db.Persons.Add(person);

db.SaveChanges();

约翰拥有汽车 1,2,3 。 John在前端更新了他的汽车,所以现在我通过了一个新的汽车ID列表,因此我进行了这样的更新(实际代码将使用模型,并且可能会调用这样的方法):

John owns cars 1,2,3. John updates his cars on frontend so now I get passed a new list of car ids so I update like this (actual code would be using a model and probably call into a method like this):

public static void UpdateCars(int personId, int[] newCars)
{
    using (var db = new PersonCarDbContext())
    {
        var person = db.Persons.Include(x => x.PersonCars).ThenInclude(x => x.Car).Single(x => x.Id == personId);

        var toRemove = person.PersonCars.Where(x => !newCars.Contains(x.CarId)).ToList();
        var toAdd = newCars.Where(c => !person.PersonCars.Any(x => x.CarId == c)).ToList();

        foreach (var pc in toRemove)
        {
            person.PersonCars.Remove(pc);
        }

        foreach (var carId in toAdd)
        {
            var pc = db.PersonCars.Add(new PersonCar { CarId = carId, PersonId = person.Id });
        }

        db.SaveChanges();
    }
}

我计算出要删除的内容,然后添加做动作。所有非常简单的内容,但在现实世界中,一个实体可能具有多个多对多的集合,即标签,类别,选项等。一个应用程序具有多个实体。每种更新方法几乎完全相同,最终我将相同的代码重复多次。例如,假设Person还具有 Category 实体多对多关系,则它看起来像这样:

I work out ones to remove, ones to add then do the actions. All very simple stuff but in real world an entity may have multiple many-to-many collections i.e. tags, categories, options etc.. and an application has several entities. Each update method is pretty much exactly the same and I end up with the same code repeated several times. For instance lets say Person also had a Category entity many-to-many relationship it would look like this:

public static void UpdateCategory(int personId, int[] newCats)
{
    using (var db = new PersonCarDbContext())
    {
        var person = db.Persons.Include(x => x.PersonCategories).ThenInclude(x => x.Category).Single(x => x.Id == personId);

        var toRemove = person.PersonCategories.Where(x => !newCats.Contains(x.CategoryId)).ToList();
        var toAdd = newCats.Where(c => !person.PersonCategories.Any(x => x.CategoryId == c)).ToList();

        foreach (var pc in toRemove)
        {
            person.PersonCategories.Remove(pc);
        }

        foreach (var catId in toAdd)
        {
            var pc = db.PersonCategories.Add(new PersonCategory { CategoryId = catId, PersonId = person.Id });
        }

        db.SaveChanges();
    }
}

这是完全相同的代码,只是引用了不同的类型和属性。我最终将这段代码重复了很多遍。我做错了还是对通用方法来说是一个好例子?

It's exactly the same code just referencing different types and properties. I am ending up with this code repeated numerous times. Am I doing this wrong or is this a good case for a generic method?

我觉得这是使用通用方法的好地方,但我不太清楚

I feel it's a good place for a generic to be used but i can't quite see how to do it.

它将需要实体的类型,连接实体的类型和外部实体的类型,因此可能需要:

It will need the type of entity, type of join entity and type of outer entity so maybe something like:

public T UpdateJoinedEntity<T, TJoin, Touter>(PersonCarDbContext db, int entityId, int[] nestedids)
{
    //.. do same logic but with reflection?
}

方法将计算出正确的属性并进行所需的删除和添加。

Method will then work out the right property and do the required removes and adds.

这可行吗?我看不到该怎么做,但看起来似乎是可行的。

Is this feasible? I can't see how to do it but it looks like something that is possible.

推荐答案

,但分解起来并不是那么简单,尤其是要考虑到不同的键类型,显式或影子FK属性等,同时保持最小的方法参数。

"All very simple stuff", but not so simple to factorize, especially taking into account different key types, explicit or shadow FK properties etc., at the same time keeping the minimum method arguments.

这是我能想到的最佳分解方法,该方法适用于具有2个显式 int FK的链接(联接)实体:

Here is the best factorized method I can think of, which works for link (join) entities having 2 explicit int FKs:

public static void UpdateLinks<TLink>(this DbSet<TLink> dbSet, 
    Expression<Func<TLink, int>> fromIdProperty, int fromId, 
    Expression<Func<TLink, int>> toIdProperty, int[] toIds)
    where TLink : class, new()
{
    // link => link.FromId == fromId
    var filter = Expression.Lambda<Func<TLink, bool>>(
        Expression.Equal(fromIdProperty.Body, Expression.Constant(fromId)),
        fromIdProperty.Parameters);
    var existingLinks = dbSet.Where(filter).ToList();

    var toIdFunc = toIdProperty.Compile();
    var deleteLinks = existingLinks
        .Where(link => !toIds.Contains(toIdFunc(link)));

    // toId => new TLink { FromId = fromId, ToId = toId }
    var toIdParam = Expression.Parameter(typeof(int), "toId");
    var createLink = Expression.Lambda<Func<int, TLink>>(
        Expression.MemberInit(
            Expression.New(typeof(TLink)),
            Expression.Bind(((MemberExpression)fromIdProperty.Body).Member, Expression.Constant(fromId)),
            Expression.Bind(((MemberExpression)toIdProperty.Body).Member, toIdParam)),
        toIdParam);
    var addLinks = toIds
        .Where(toId => !existingLinks.Any(link => toIdFunc(link) == toId))
        .Select(createLink.Compile());

    dbSet.RemoveRange(deleteLinks);
    dbSet.AddRange(addLinks);
}

所需的只是连接实体 DbSet ,两个表示FK属性的表达式以及所需的值。属性选择器表达式用于动态构建查询过滤器,以及构成和编译函子以创建和初始化新的链接实体。

All it needs is the join entity DbSet, two expressions representing the FK properties, and the desired values. The property selector expressions are used to dynamically build query filters as well as composing and compiling a functor to create and initialize new link entity.

代码并不难,但是需要 System.Linq.Expressions.Expression 方法知识。

The code is not that hard, but requires System.Linq.Expressions.Expression methods knowledge.

与手写代码的唯一区别是

The only difference with handwritten code is that

Expression.Constant(fromId)

内部 filter 表达式将导致EF生成使用常量值而不是参数的SQL查询,这将阻止查询计划缓存。可以通过将上面的内容替换为

inside filter expression will cause EF generating a SQL query with constant value rather than parameter, which will prevent query plan caching. It can be fixed by replacing the above with

Expression.Property(Expression.Constant(new { fromId }), "fromId")

话虽如此,样本的用法如下:

With that being said, the usage with your sample would be like this:

public static void UpdateCars(int personId, int[] carIds)
{
    using (var db = new PersonCarDbContext())
    {
        db.PersonCars.UpdateLinks(pc => pc.PersonId, personId, pc => pc.CarId, carIds);
        db.SaveChanges();
    }
}

以及其他方法:

public static void UpdatePersons(int carId, int[] personIds)
{
    using (var db = new PersonCarDbContext())
    {
        db.PersonCars.UpdateLinks(pc => pc.CarId, carId, pc => pc.PersonId, personIds);
        db.SaveChanges();
    }
}

这篇关于更新EFCore联接的通用方法的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆