Дерево импорта / экспорта Entity Framework Core сущностей с похожими внешними ключами

Мне нужно добавить функцию импорта / экспорта в мое приложение 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 ...

Заранее благодарим за ответы.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
406
1

Ответы 1

Предположим, у вас есть список импортируемых 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

Спасибо за ответ. Проблема в том, что вы импортируете в новую базу данных те же идентификаторы, что и в предыдущей ... так что я могу получить конфликт! Я хочу получить такие новые идентификаторы, сгенерированные целевой базой данных, поэтому немного сложно хранить внешние ключи ... Потому что мое единственное решение сказать EntityFramework, что два вновь созданных объекта в базе данных одинаковы, - иметь точно одна и та же ссылка каждый раз, когда объект используется в качестве внешнего ключа.

Fabien Dezautez 15.11.2018 09:10

@FabienDezautez, Итак, если я правильно понимаю, категория {Id:99, Name:"Category #1"} в вашем json точно такая же, как {Id:66, Name:"Category #1"} в базе данных?

itminus 15.11.2018 09:19

Нет, категории 99 и 66 разные. Но эти идентификаторы поступают из исходной базы данных и уже могут быть использованы в целевой базе данных.

Fabien Dezautez 15.11.2018 09:21

@FabienDezautez Подумайте о том, что у вас есть [{Id:66, Name:"Category #1"},{Id:99, Name:"Category #1"}] в исходной базе данных, и id = 66 уже был принят базой данных dest, а новая вставленная запись для id = 66 будет [{Id:1106, Name:"Category #1"}], как насчет id = 99 из исходной базы данных? их следует рассматривать как одно и то же?

itminus 15.11.2018 09:27

@FabienDezautez Если идентификатор src'id уже был использован базой данных dest, и их имена точно такие же, следует ли считать их одинаковыми? Если нет, то в каком случае следует считать, что они дублируются?

itminus 15.11.2018 09:31

@FabienDezautez Прежде чем кто-либо сможет сделать предложение, мы должны понять, если пользователь импортирует json несколько раз, как вы можете определить, дублируется ли запись из исходной базы данных. Поскольку идентификатор из исходной базы данных, вероятно, берется целевой базой данных, мы должны проверить его другими способами. Если вы можете управлять сериализацией из исходной базы данных, я рекомендую вам добавить HASH-файл в качестве отпечатка пальца или просто использовать поле Name в качестве идентификатора.

itminus 15.11.2018 09:51

Фактически, у меня есть две возможности. Во-первых: я хочу, чтобы, если я найду категорию с тем же именем (но, конечно, с другим идентификатором), я заменю другие поля категории dest значениями, заданными из src. Второй вариант: я использую существующую категорию без обновления. В конечном итоге json можно импортировать во второй раз, но пользователю нужно будет указать, объединяются ли конфликты или нет.

Fabien Dezautez 15.11.2018 10:55

@FabienDezautez Значит, поле Name будет идентификатором для 1-го варианта?

itminus 15.11.2018 11:01

Да, я могу использовать Имя в качестве идентификатора. Но моя проблема в том, что мне придется перебирать каждый продукт, чтобы добавить все отдельные категории. И если я не удалю категорию для каждого продукта перед передачей продукта в DbContext, дублированные категории будут конфликтовать, потому что десериализатор будет создавать разные экземпляры. Я ошибся ?

Fabien Dezautez 15.11.2018 11:25

На самом деле я думаю, что должен сказать своему сериализатору, что в дереве объектов для десериализации, если он находит два объекта с одинаковыми ключами, он должен использовать одну и ту же ссылку на память, а не создавать новый каждый раз. В противном случае dbcontext будет отслеживать две ссылки и даст конфликт «дублированный ключ».

Fabien Dezautez 15.11.2018 11:25

@FabienDezautez Это не подходит. Поскольку это не мешает пользователям импортировать одни и те же записи в разные JSON несколько раз. Проверка должна выполняться в целевой базе данных.

itminus 15.11.2018 11:29

Другие вопросы по теме