Мне нужно добавить функцию импорта / экспорта в мое приложение ASP.NET Core.
Я бы хотел взять объекты в одну базу данных, экспортировать эти объекты в один файл, а затем импортировать этот файл в новую базу данных.
Моя проблема в том, что у меня есть объекты, которые имеют одинаковый внешний ключ. Вот простая модель, иллюстрирующая то, что я хочу делать:
public class Bill
{
public List<Product> Products { get; set; }
public int Id { get; set; }
...
}
public class Product
{
public int Id { get; set; }
public int ProductCategoryId { get; set; }
public ProductCategory ProductCategory { get; set; }
...
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
Я хочу экспортировать счет для импорта в другую среду моего приложения. Итак, если я экспортирую счет, я получу такой Json:
{
"id": 1,
"products" : [
{
"id" : 1,
"productCategoryId": 1,
"productCategory": {
"id" : 1,
"name" : "Category #1"
}
},
{
"id" : 2,
"productCategoryId": 1,
"productCategory": {
"id" : 1,
"name" : "Category #1"
}
},
{
"id" : 3,
"productCategoryId": 1,
"productCategory": {
"id" : 2,
"name" : "Category #2"
}
}
]
}
Если я десериализую этот json в сущности в моей новой среде (конечно, игнорируя сопоставление идентификаторов), я получу три новые категории (категория будет продублирована для продукта 1 и 2), потому что сериализатор создаст две категории ...
Поэтому, когда я помещаю его в свою базу данных, он добавит 3 строки вместо 2 в таблицу Category ...
Заранее благодарим за ответы.





Предположим, у вас есть список импортируемых Category, вы можете сначала получить список идентификаторов этих категорий, а затем запросить базу данных, чтобы убедиться, что они уже сохранены в базе данных. А те, которые уже есть, просто пропустите (или обновите по своему усмотрению).
Поскольку у нас есть несколько типов сущностей (категории, продукты, счета и потенциальные BillProducts), вместо того, чтобы писать новый импортер для каждого TEntity, я предпочитаю писать общий метод Importer для работы с любым списком типов сущностей с Generic и Reflection:
public async Task ImportBatch<TEntity,TKey>(IList<TEntity> entites)
where TEntity : class
where TKey: IEquatable<TKey>
{
var ids = entites.Select( e=> GetId<TEntity,TKey>(e));
var existingsInDatabase=this._context.Set<TEntity>()
.Where(e=> ids.Any(i => i.Equals(GetId<TEntity,TKey>(e)) ))
.ToList();
using (var transaction = this._context.Database.BeginTransaction())
{
try{
this._context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT " + typeof(TEntity).Name + " ON;");
this._context.SaveChanges();
foreach (var entity in entites)
{
var e= existingsInDatabase.Find(existing => {
var k1 =GetId<TEntity,TKey>(existing);
var k2=GetId<TEntity,TKey>(entity);
return k1.Equals(k2);
});
// if not yet exists
if (e == null){
this._context.Add(entity);
}else{
// if you would like to update the old one when there're some differences
// uncomment the following line :
// this._context.Entry(e).CurrentValues.SetValues(entity);
}
}
await this._context.SaveChangesAsync();
transaction.Commit();
}
catch{
transaction.Rollback();
}
finally{
this._context.Database.ExecuteSqlCommand($"SET IDENTITY_INSERT " + typeof(TEntity).Name + " OFF;");
await this._context.SaveChangesAsync();
}
}
return;
}
Здесь GetId<TEntity,TKey>(TEntity e) - это простой вспомогательный метод, который используется для получения файла key от e:
// use reflection to get the Id of any TEntity type
private static TKey GetId<TEntity,TKey>(TEntity e)
where TEntity : class
where TKey : IEquatable<TKey>
{
PropertyInfo pi=typeof(TEntity).GetProperty("Id");
if (pi == null) { throw new Exception($"the type {typeof(TEntity)} must has a property of `Id`"); }
TKey k = (TKey) pi.GetValue(e);
return k ;
}
Чтобы сделать код более пригодным для повторного использования, мы можем создать сервис EntityImporter, содержащий описанный выше метод:
public class EntityImporter{
private DbContext _context;
public EntityImporter(DbContext dbContext){
this._context = dbContext;
}
public async Task ImportBatch<TEntity,TKey>(IList<TEntity> entites)
where TEntity : class
where TKey: IEquatable<TKey>
{
// ...
}
public static TKey GetId<TEntity,TKey>(TEntity e)
where TEntity : class
where TKey : IEquatable<TKey>
{
// ...
}
}
а затем зарегистрируйте службы во время запуска:
services.AddScoped<DbContext, AppDbContext>();
services.AddScoped<EntityImporter>();
Прецедент :
Во-первых, я возьму для примера несколько категорий:
var categories = new ProductCategory[]{
new ProductCategory{
Id = 1,
Name = "Category #1"
},
new ProductCategory{
Id = 2,
Name = "Category #2"
},
new ProductCategory{
Id = 3,
Name = "Category #3"
},
new ProductCategory{
Id = 2,
Name = "Category #2"
},
new ProductCategory{
Id = 1,
Name = "Category #1"
},
};
await this._importer.ImportBatch<ProductCategory,int>(categories);
Ожидаемые результаты должны быть импортированы только 3 строками:
1 category #1
2 category #2
3 category #3
А вот на скриншоте все работает:
Наконец, для ваших счетов json вы можете сделать, как показано ниже, чтобы импортировать элементы:
var categories = bill.Products.Select(p=>p.ProductCategory).ToList();
var products = bill.Products.ToList();
// other List<TEntity>...
// import the categories firstly , since they might be referenced by other entity
await this._importer.ImportBatch<ProductCategory,int>(categories);
// import the product secondly , since they might be referenced by BillProduct table
await this._import.ImportBatch<Product,int>(products);
// ... import others
@FabienDezautez, Итак, если я правильно понимаю, категория {Id:99, Name:"Category #1"} в вашем json точно такая же, как {Id:66, Name:"Category #1"} в базе данных?
Нет, категории 99 и 66 разные. Но эти идентификаторы поступают из исходной базы данных и уже могут быть использованы в целевой базе данных.
@FabienDezautez Подумайте о том, что у вас есть [{Id:66, Name:"Category #1"},{Id:99, Name:"Category #1"}] в исходной базе данных, и id = 66 уже был принят базой данных dest, а новая вставленная запись для id = 66 будет [{Id:1106, Name:"Category #1"}], как насчет id = 99 из исходной базы данных? их следует рассматривать как одно и то же?
@FabienDezautez Если идентификатор src'id уже был использован базой данных dest, и их имена точно такие же, следует ли считать их одинаковыми? Если нет, то в каком случае следует считать, что они дублируются?
@FabienDezautez Прежде чем кто-либо сможет сделать предложение, мы должны понять, если пользователь импортирует json несколько раз, как вы можете определить, дублируется ли запись из исходной базы данных. Поскольку идентификатор из исходной базы данных, вероятно, берется целевой базой данных, мы должны проверить его другими способами. Если вы можете управлять сериализацией из исходной базы данных, я рекомендую вам добавить HASH-файл в качестве отпечатка пальца или просто использовать поле Name в качестве идентификатора.
Фактически, у меня есть две возможности. Во-первых: я хочу, чтобы, если я найду категорию с тем же именем (но, конечно, с другим идентификатором), я заменю другие поля категории dest значениями, заданными из src. Второй вариант: я использую существующую категорию без обновления. В конечном итоге json можно импортировать во второй раз, но пользователю нужно будет указать, объединяются ли конфликты или нет.
@FabienDezautez Значит, поле Name будет идентификатором для 1-го варианта?
Да, я могу использовать Имя в качестве идентификатора. Но моя проблема в том, что мне придется перебирать каждый продукт, чтобы добавить все отдельные категории. И если я не удалю категорию для каждого продукта перед передачей продукта в DbContext, дублированные категории будут конфликтовать, потому что десериализатор будет создавать разные экземпляры. Я ошибся ?
На самом деле я думаю, что должен сказать своему сериализатору, что в дереве объектов для десериализации, если он находит два объекта с одинаковыми ключами, он должен использовать одну и ту же ссылку на память, а не создавать новый каждый раз. В противном случае dbcontext будет отслеживать две ссылки и даст конфликт «дублированный ключ».
@FabienDezautez Это не подходит. Поскольку это не мешает пользователям импортировать одни и те же записи в разные JSON несколько раз. Проверка должна выполняться в целевой базе данных.
Спасибо за ответ. Проблема в том, что вы импортируете в новую базу данных те же идентификаторы, что и в предыдущей ... так что я могу получить конфликт! Я хочу получить такие новые идентификаторы, сгенерированные целевой базой данных, поэтому немного сложно хранить внешние ключи ... Потому что мое единственное решение сказать EntityFramework, что два вновь созданных объекта в базе данных одинаковы, - иметь точно одна и та же ссылка каждый раз, когда объект используется в качестве внешнего ключа.