Entity Framework Базовое дерево импорта / экспорта объектов с похожими внешними ключами - PullRequest
0 голосов
/ 14 ноября 2018

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

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

1 Ответ

0 голосов
/ 15 ноября 2018

Предположим, у вас есть список Category для импорта, вы можете сначала получить список идентификаторов этих категорий, а затем запросить базу данных, чтобы убедиться, что она уже сохранена в базе данных. А для тех, которые уже существуют, просто пропустите их (или обновите их, как вам нравится).

Так как у нас есть несколько типов сущностей (категорий, продуктов, счетов и потенциальных счетов-фактур), а не пишем новый импортер для каждого 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

А вот скриншот, он работает:

enter image description here

Наконец, для ваших счетов 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
...