Создание динамического выражения для сравнения двух таблиц
У меня есть две таблицы, таблица 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 (поскольку вы используете
IEnumerable
s) запрос, например
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();