Correct way to remove a many-to-many relationship via linq to sql?

前端 未结 3 1880
广开言路
广开言路 2021-01-20 19:48

Let\'s say we have two tables with a many-to-many relationship:

public class Left{ /**/ }

public class Right{ /**/ }

public class LeftRight{ /**/ }
         


        
相关标签:
3条回答
  • 2021-01-20 20:25

    Here is a 'little' extension method I wrote to simplify this problem:

      public static class EntitySetExtensions
      {
        public static void UpdateReferences<FK, FKV>(
            this EntitySet<FK> refs,
            Func<FK, FKV> fkvalue,
            Func<FKV, FK> fkmaker,
            Action<FK> fkdelete,
            IEnumerable<FKV> values)
          where FK : class
          where FKV : class
        {
          var fks = refs.Select(fkvalue).ToList();
          var added = values.Except(fks);
          var removed = fks.Except(values);
    
          foreach (var add in added)
          {
            refs.Add(fkmaker(add));
          }
    
          foreach (var r in removed)
          {
            var res = refs.Single(x => fkvalue(x) == r);
            refs.Remove(res);
            fkdelete(res);
          }
        }
      }
    

    It could probably be improved, but it has served me well :)

    Example:

    Left entity = ...;
    IEnumerable<Right> rights = ...;
    
    entity.LeftRights.UpdateReferences(
     x => x.Right, // gets the value
     x => new LeftRight { Right = x }, // make reference
     x => { x.Right = null; }, // clear references
     rights);
    

    Algorithm description:

    Suppose A and B is many-to-many relationship, where AB would be the intermediary table.

    This will give you:

    class A { EntitySet<B> Bs {get;} }
    class B { EntitySet<A> As {get;} }
    class AB { B B {get;} A A {get;} }
    

    You now have an object of A, that reference many B's via AB.

    1. Get all the B from A.Bs via 'fkvalue'.
    2. Get what was added.
    3. Get what was removed.
    4. Add all the new ones, and construct AB via 'fkmaker'.
    5. Delete all the removed ones.
    6. Optionally, remove other referenced objects via 'fkdelete'.

    I would like to improve this by using Expression instead, so I could 'template' the method better, but it would work the same.

    0 讨论(0)
  • 2021-01-20 20:34

    Take two, using expressions:

    public static class EntitySetExtensions
    {
      public static void UpdateReferences<FK, FKV>(
          this EntitySet<FK> refs,
          Expression<Func<FK, FKV>> fkexpr,
          IEnumerable<FKV> values)
        where FK : class
        where FKV : class
      {
        Func<FK, FKV> fkvalue = fkexpr.Compile();
        var fkmaker = MakeMaker(fkexpr);
        var fkdelete = MakeDeleter(fkexpr);
    
        var fks = refs.Select(fkvalue).ToList();
        var added = values.Except(fks);
        var removed = fks.Except(values);
    
        foreach (var add in added)
        {
          refs.Add(fkmaker(add));
        }
    
        foreach (var r in removed)
        {
          var res = refs.Single(x => fkvalue(x) == r);
          refs.Remove(res);
          fkdelete(res);
        }
      }
    
      static Func<FKV, FK> MakeMaker<FKV, FK>(Expression<Func<FK, FKV>> fkexpr)
      {
        var me = fkexpr.Body as MemberExpression;
    
        var par = Expression.Parameter(typeof(FKV), "fkv");
        var maker = Expression.Lambda(
            Expression.MemberInit(Expression.New(typeof(FK)), 
              Expression.Bind(me.Member, par)), par);
    
        var cmaker = maker.Compile() as Func<FKV, FK>;
        return cmaker;
      }
    
      static Action<FK> MakeDeleter<FK, FKV>(Expression<Func<FK, FKV>> fkexpr)
      {
        var me = fkexpr.Body as MemberExpression;
        var pi = me.Member as PropertyInfo;
    
        var par = Expression.Parameter(typeof(FK), "fk");
        var maker = Expression.Lambda(
            Expression.Call(par, pi.GetSetMethod(), 
              Expression.Convert(Expression.Constant(null), typeof(FKV))), par);
    
        var cmaker = maker.Compile() as Action<FK>;
        return cmaker;
      }
    }
    

    Now the usage is uber simple! :)

    Left entity = ...;
    IEnumerable<Right> rights = ...;
    
    entity.LeftRights.UpdateReferences(x => x.Right, rights);
    

    The first expression is now used to establish the 'relationship'. From there I can infer the 2 previously required delegates. Now no more :)

    Important:

    To get this to work properly in Linq2Sql, you need to mark the associations from intermediary table with 'DeleteOnNull="true"' in the dbml file. This will break the designer, but still works correctly with SqlMetal.

    To unbreak the designer, you need to remove those additional attributes.

    0 讨论(0)
  • 2021-01-20 20:45

    Personally, I'd replace

    left.LeftRrights.Remove(relation.First());
    

    with

    Db.LeftRights.DeleteAllOnSubmit(relation)
    

    because it seems more obvious what's going to happen. If you are wondering what the behaviour of ".Remove" is now, you'll be wondering anew when you look at this code in 6 months time.

    0 讨论(0)
提交回复
热议问题