Создание динамического выражения для сравнения двух таблиц

У меня есть две таблицы, таблица A и таблица B, которые имеют одинаковую схему.

Я хочу сравнить обе таблицы из разных контекстов. Если запись в таблице B не существует в таблице A, она вставит ее в контекст A.

То, что я должен выяснить, похожи один тип стола.

      var a = context.Set<T>().AsEnumerable();
var b = context2.Set<T>().AsEnumerable();

var NotExistsOnA = b.Where(bRecord => 
                          !a.Any(aRecord =>
                                 bRecord.xxxId.Equals(aRecord.xxxId))).ToList();

Как я могу создать динамическое выражение, чтобы код можно было повторно использовать, потому что для сравнения будет использоваться несколько таблиц. Я застрял в том, как обрабатывать часть xxxId, поскольку разные таблицы будут иметь другое имя первичного ключа.

Любая помощь приветствуется.

1 ответ

Службы метаданных EF Core могут использоваться для получения информации о первичном ключе объекта, например

      var entityType = context.Model.FindEntityType(typeof(T));
var primaryKey = entityType.FindPrimaryKey();

Затем эту информацию можно использовать для динамического построения компаратора равенства:

      public static Expression<Func<T, T, bool>> GetPrimaryKeyCompareExpression<T>(this DbContext context)
{
    var entityType = context.Model.FindEntityType(typeof(T));
    var primaryKey = entityType.FindPrimaryKey();
    var first = Expression.Parameter(typeof(T), "first");
    var second = Expression.Parameter(typeof(T), "second");
    var body = primaryKey.Properties
        .Select(p => Expression.Equal(
            Expression.Property(first, p.PropertyInfo),
            Expression.Property(second, p.PropertyInfo)))
        .Aggregate(Expression.AndAlso); // handles composite PKs
    return Expression.Lambda<Func<T, T, bool>>(body, first, second);
}

который, в свою очередь, может быть скомпилирован для делегирования и использования внутри LINQ to Objects (поскольку вы используете IEnumerables) запрос, например

      var setA = context.Set<T>().AsEnumerable();
var setB = context2.Set<T>().AsEnumerable();

var comparePKs = context.GetPrimaryKeyCompareExpression<T>().Compile();
var notExistsOnA = setB
    .Where(b => !setA.Any(a => comparePKs(a, b)))
    .ToList();

Но обратите внимание, что в LINQ to Objects, !Any(...) внутренний критерий запроса неэффективен, так как это операция линейного поиска, поэтому результирующая временная сложность квадратична ( O(Na * Nb), поэтому у вас будут проблемы с производительностью с большими наборами данных.

В общем, было бы лучше использовать оператор соединения. Но вместо сравнения ему нужен селектор ключей, который также может быть построен аналогично приведенному выше, но на этот раз с выдачей Tuple экземпляры (для обработки составных PK), например

      public static Expression<Func<T, object>> GetPrimaryKeySelector<T>(this DbContext context)
{
    var entityType = context.Model.FindEntityType(typeof(T));
    var primaryKey = entityType.FindPrimaryKey();
    var item = Expression.Parameter(typeof(T), "item");
    var body = Expression.Call(
        typeof(Tuple), nameof(Tuple.Create),
        primaryKey.Properties.Select(p => p.ClrType).ToArray(),
        primaryKey.Properties.Select(p => Expression.Property(item, p.PropertyInfo)).ToArray()
    );
    return Expression.Lambda<Func<T, object>>(body, item);
}

и может использоваться следующим образом

      var selectPK = context.GetPrimaryKeySelector<T>().Compile();
var notExistsOnA = setB
    .GroupJoin(setA, selectPK, selectPK, (b, As) => (b, As))
    .Where(r => !r.As.Any())
    .Select(r => r.b)
    .ToList();
Другие вопросы по тегам