Представляем вашему вниманию DbTool — утилиту командной строки для экспорта данных БД в различные форматы и open-source библиотеку Korzh.DbUtils, использование которых может значительно упростить первоначальное "засевание" базы данных в вашем .NET (Core) приложении.


С помощью этого набора инструментов вы сможете:


  1. Сохранить данные из вашей локальной БД в файлы некого текстового формата (XML, JSON), которые легко подключить к проекту.
  2. Использовать сохранённые файлы для заполнения базы данных самого приложения при его первом старте.

Ниже я расскажу зачем все это нужно, как проинсталлировать и настроить данные инструменты и опишу детальный сценарий их использования.


image


Зачем мы сделали DbTool


Первоначальной задачей было создание удобного механизма наполнения БД в .NET (Core) приложениях. В силу специфики нашего рода деятельности (разработка компонент) нам часто приходится создавать небольшие приложения-примеры, которые демонстрируют ту или иную особенность нашего продукта. Такие демо-проекты должны работать с некой тестовой базой данных и поэтому желательно автоматически создать и наполнить эту БД при первом старте приложения.


Если в проекте используется Entity Framework (Core) (а так оно чаще всего и бывает), то с созданием базы никаких проблем нет. Вы просто вызываете dbContext.Database.EnsureCreated или dbContext.Database.Migrate (если важно сохранить миграции).


А вот с наполнением базы данными все немного сложнее. Первое, что приходит в голову, это просто создать SQL скрипт с кучей INSERT'ов, положить его в проект и выполнять при первом запуске. Это работает (и долгое время мы так и делали), но с таким подходом есть некоторые проблемы. В первую очередь — вопрос синтаксиса SQL под конкретную СУБД. Довольно часто исходная СУБД отличается от той, которая реально используется у пользователя и наш SQL скрипт может не срабатывать.


Вторая возможная проблема — это миграции самой базы. Периодически возникает необходимость немного поменять структуру БД (добавить новое поле, удалить или переименовать старое, добавить новую связь между таблицами и т.д.). SQL script созданный под старую структуру обычно становится в этом случае нерелевантен и его выполнение вызывает ошибку. В то время, как загрузка данных из некоторого стороннего формата проходит без проблем. Новые/измененные поля просто пропускаются. Согласитесь, что в целях демонстрации лучше чтобы программа запустилась, хоть и без данных в каком-то новом поле, нежели чтобы не запустилась совсем.


В результате мы пришли к следующему решению:


  1. Данные из "мастер-копии" нашей демонстрационной БД записываются в файле в неком "независимом" формате (на данный момент это XML или JSON). Полученные файлы (или один файл архива) поставляются вместе с проектом. Этой задачей, собственно, и занимается DbTool.
  2. В нашу программу вставляется небольшой кусок кода, который с помощью классов и функций библиотеки Korzh.DbUtils наполняет базу данными из файла(ов) полученных на первом шаге.
    Кроме описанного выше сценария DbTool можно использовать просто для экспорта данных в другие форматы и для переноса данных между БД. Так, к примеру можно выгрузить данные из вашей БД на SQL Server и потом загрузить в похожую БД на MySQL

Инсталляция


DbTool реализована как .NET Core global tool т.е. может быть легко установлена на любую систему где есть .NET SDK верcии 2.1 или выше.


Для инсталляции утилиты нужно просто открыть консоль (Terminal / Command Prompt) и запустить такую команду:


dotnet tool install -g Korzh.DbTool

Для проверки после установки наберите в консоли dbtool и вы увидите справку со списком доступных команд.



Добавляем соединение с базой


Чтобы начать работать с DbTool нужно добавить соединение с базой данных:


dbtool add {YourConnectionId} {DbType} {YourConnectionString}

Здесь:


  • {YourConnectionId} — это некий идентификатор, который вы хотите назначить этому соединению, чтобы обращаться к нему в дальнейшем при запуске других команд.
  • DbType — тип вашей СУБД. На момент написания этой статьи DbTool (версия 1.1.7) поддерживал базы данных SQL Server (mssql) и MySQL (mysql).
  • Последний параметр в этой команде — это строка подключения (connection string). Такая же, которую вы уже используете в своем .NET (Core) проекте.

Пример:



После этого вы можете проверить все ваши соединения, набрав:


dbtool connections list

Экспорт данных


Теперь, когда мы добавили соединение, мы можем экспортировать нашу базу данных с помощью команды export:


dbtool export {ConnectionId} [--format=xml|json] [--output={path-to-folder}] [--zip={file-name}]

Любая опция, указанная выше, может быть опущена. Если не указывать format то будет использоваться JSON. Если опустить опцию output, то результат будет помещен в каталог вида ConnectionId_yyyy-MM-dd в незапакованном виде.


Например, следующая команда:


dbtool export MyDb01 --zip=MyDbData.zip

создаст ZIP-архив с именем MyDbData.zip в текущем каталоге и заполнит его файлами данных в формате JSON (по одному файлу на каждую таблицу БД).



Импорт данных


Вы можете импортировать данные, созданные на предыдущем шаге, обратно в вашу БД. Или в любую другую базу с такой же структурой.


Важно: DbTool не создает таблицы во время операции импорта. Таким образом, база данных, в которую импортируются данные, уже должна существовать и иметь ту же (или хотя бы похожую) структуру, что и исходная.

Сама команда импорта выглядит следующим образом:


dbtool import {ConnectionId} [--input=path-to-file-or-folder] [--format=xml|json]

Опция --input указывает утилите где искать импортируемые данные. Если указан путь к папке, DbTool будет импортировать .xml или .json файлы в этой папке. Если же это ZIP-файл, то утилита сначала распакует этот архив и оттуда заберет необходимые файлы с данными.


Как и в предыдущем случае --format может быть опущена так как DbTool может распознавать формат по расширениям файлов.


Пример:


dbtool import MyDb01 --input=MyDbData.zip

Бибилиотека Korzh.DbUtils


Сама утилита DbTool построена на основе open-source библиотеки Korzh.DbUtils, которая включает в себя несколько пакетов с реализацией некоторых базовых операций над базами данных.


Korzh.DbUtils


Определяет основные абстракции и интерфейсы, такие как IDatasetExporter, IDatasetImporter, IDataPacker, IDbBridge


Korzh.DbUtils.Import


Содержит реализации интерфейсов IDatasetImporter для форматов XML и JSON. Кроме того, этот пакет включает класс DbInitializer, который можно использовать для заполнения данных в ваших проектах (подробнее об этом ниже).


Korzh.DbUtils.Export


Содержит реализации IDatasetExporter для XML и JSON.


Korzh.DbUtils.SqlServer


Содержит реализацию интерфейсов основных операций с БД (IDbBridge, IDbReader, IDbSeeder) для MS SQL Server.


Korzh.DbUtils.MySQL


Содержит реализации интерфейсов по работе с БД для MySQL.


Здесь вы можете найти полный справочник по API библиотеки Korzh.DbUtils.


Использование Korzh.DbUtils для заполнение БД данными при старте приложения


Теперь, собственно рассмотрим как с помощью DbTool и Korzh.DbUtils реализовать базовый сценарий наполнения (seeding) БД при первом запуске приложения.


Предположим, у вас есть «мастер-копия» некоторой БД, которую необходимо "скопировать" на компьютере пользователя при первом запуске приложения.


Шаг 1: Экспортируем мастер-копию в JSON


Просто устанавливаем DbTool, как описано выше, добавляем соединение с БД и запускаем команду экспорта, чтобы сохранить все данные из этой БД в отдельную папку:


dotnet tool install -g Korzh.DbTool

dbtool connections add MyMasterDb mssql "{ConnectionString}"

dbtool export MyMasterDb

Шаг 2: Добавляем файлы с данными в наш проект


После предыдущего шага у нас есть новая папка, вида MyMasterDb-yyyy-MM-dd, с кучей JSON файлов (по одному для каждой таблицы). Просто копируем содержимое этой папки в App_Data\DbSeed нашего .NET (Core) проекта. Обратите внимание, что для проектов под .NET Framework вам также нужно будет "вручную" добавить эти файлы в проект.


Шаг 3: Код инициализации БД


Хотя сам процесс (с точностью до некоторых деталей) применим для любого типа проекта под .NET Core или .NET Framework (версии 4.6.1 или выше), для простоты описания предположим, что речь идет про проект ASP.NET Core, который работает с базой данных SQL Server и что эта база создается автоматически, с помощью Entity Framework Core.


Таким образом для решения задачи заполнения БД данными при первом запуске нам нужно:


1. Установить в проект NuGet пакеты библиотеки Korzh.DbUtils


В этом случае нам понадобится 2 из них:


  • Korzh.DbUtils.Import
  • Korzh.DbUtils.SqlServer

2. Добавить код инициализации


Вот пример такого кода, который мы должны добавить в конце метода Startup.Configure:


public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    .     .     .     .
    app.UseMvc();

    using (var scope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
    using (var context = scope.ServiceProvider.GetService<AppDbContext>()) {
        if (context.Database.EnsureCreated()) { //run only if database was not created previously
            Korzh.DbUtils.DbInitializer.Create(options => {
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")); //set the connection string for our database
                options.UseFileFolderPacker(System.IO.Path.Combine(env.ContentRootPath, "App_Data", "SeedData")); //set the folder where to get the seeding data
            })
            .Seed();
        }
    }
}

Чтобы сделать уж совсем все красиво или если надо выполнить некоторую дополнительную инициализацию при первом запуске (например, добавить несколько учетных записей и/или ролей пользователей), то лучше оформить весь этот код в виде отдельного extension method'а (назовем его EnsureDbInitialized) для интерфейса IApplicationBuilder.


Пример такой реализации можно найти на GitHub в демо проекте для библиотеки EasyQuery.


В этом случае вам просто нужно добавить один вызов в конце вашего метода Startup.Configure:


public void Configure (приложение IApplicationBuilder, окружающая среда IHostingEnvironment)
{
      .    .    .    .
     app.UseMvc ();

     //Init database (only if necessary)
     app.EnsureDbInitialized(Configuration, env);
}

Планы на будущее


Хотя библиотека и утилита были написаны под вполне определенный сценарий, мы старались сделать все как можно более гибко и расширяемо поэтому включить дополнительный функционал не составит особых проблем.


Из возможных улучшений мы видим следующие:


  • Поддержка других баз данных (PostgreSQL, Oracle, SQLite, MariaDB)


  • Новые форматы в которые можно экспортировать данные (CSV, Excel, HTML)


  • Операция непосредственного копирования данных из БД в БД (сейчас можно реализовать через пару последовательных вызовов команд export/import)


  • Полноценные операции backup / restore с полным сохранением структуры БД и созданием ее "с нуля" при восстановлении.



Будем рады услышать любые пожелания или замечания и очень благодарны за новые звездочки для GitHub репозитория библиотеки :)


Спасибо за внимание!

Комментарии (5)


  1. Dimtry44
    06.09.2019 21:37

    Имеет смысл добавиьь возможность наложить diff между существующими данными и обновлёнными. Первоначальная популяция данных это хорошо, но последующие обновления/синхронизация не менее важно.


    1. korzh Автор
      09.09.2019 11:50

      Имеется в виду добавить такой функционал в библиотеку?

      Честно говоря подобный сценарий пока не рассматривался и я не совсем представляю ситуацию когда такое может понадобится. С изменениями в структуре БД и сопутствующими им добавлениями/изменениями в данных неплохо справляются миграции в EF (Core).

      Если надо добавить некоторые данные по-умолчанию, которых не было в первой версии продукта, но они появились позже — то DbInitializer с таким справится. Не будет только удаления того, чего нет в «мастер» базе и это, наверное, хорошо.

      В общем, хотелось лучше понять сценарий, когда нужен именно diff.


  1. korzh Автор
    06.09.2019 22:47

    Имеется в виду добавить такой функционал в библиотеку?

    Честно говоря подобный сценарий пока не рассматривался и я не совсем представляю ситуацию когда такое может понадобится. С изменениями в структуре БД и сопутствующими им добавлениями/изменениями в данных неплохо справляются миграции в EF (Core).

    Если надо добавить некоторые данные по-умолчанию, которых не было в первой версии продукта, но они появились позже — то DbInitializer с таким справится. Не будет только удаления того, чего нет в «мастер» базе и это, наверное, хорошо.

    В общем, хотелось лучше понять сценарий, когда нужен именно diff.


  1. Imbecile
    07.09.2019 00:49

    Статью глянул по диагонали, но. Если у вас уже EF, то зачем привязка к конкретным СУБД? Вычитали эталонную базу в объектную модель. Сериализовали в JSON/XML файл. При развёртывании — в обратном порядке. EF позаботиться о том, чтобы можно было с любой БД работать. Если поменялась схема, то это всё равно решается на уровне чтения файла с сериализованными данными.


    1. korzh Автор
      07.09.2019 19:08

      Была такая мысль. Если глянете в репозиторий, то увидите, что там даже есть проект Korzh.DbUtils.EntityFrameworkCore, который, правда, пока так и не опубликован в качестве NuGet пакета. Причин этому несколько:

      1. Не все проекты используют EF (Core). Есть еще Dapper, NPoco, Massive и куча других micro-ORM. Есть проекты, которым просто надо выполнять пару SQL запросов и они делают это просто через DbConnection/DbCommand. Но инициализировать БД нужно и в этом случае.

      2. Сделать отдельные «мосты» к разным БД все равно пришлось по двум причинам:

      • Собственно, для утилиты командной строки DbTool
      • Для импорта данных в БД, в таблицах которых есть ключи с автогенерацией и внешние ключи (foreign keys). То есть практически для любых БД. В этом случае при записи в таблицу нужно временно отключать проверку ограничений (типа `SET IDENTITY_INSERT TableName OFF`). Для разных БД это делается по разному — поэтому все равно нужен был дополнительный пакет с этим функционалом для каждой конкретной СУБД.