Одно я могу сказать точно: миграция данных между двумя БД - это одна из, если не самая сложная часть при смене СУБД или схемы базы данных. И что-то мне подсказывает, что Вы не фанат громоздких SQL конструкций.
Варианты основаны на использовании PostgreSQL и EF Core но сохраняют свою актуальность даже если на проекте нет ни строчки .Net кода или стоит другая СУБД.
Варианты без C# кода.
Эти варианты полезны если вам не нужно кардинально менять схему, либо вы всё ещё готовы писать SQL.
Вариант 1: Бэкап данных на источнике с восстановлением данных из бэкапа на БД получателе
Основан на том что в бэкап можно включить данные, не включая более ничего. Работает если схема таблиц на исходной и получателе идентична.
При создании можно указать несколько таблиц если используется консольная команда.
Пример команды:
pg_dump.exe --file "D:\\Backups\\books" --host "localhost" --port "5432" --username "postgres" --no-password --verbose --format=c --blobs --section=data --table "dbo.books" --table "dbo.flowers" "SourceDatabase"
Восстановить бэкап можно через консоль с помощью pg_restore
, либо через UI, используя такие инструменты как pgAdmin.
Минусы
Требует выгрузки на диск, что при использовании с серверами клиентов может создать проблем с допусками, пропускной способность и т.д. Но как самый примитивный вариант - он самый надёжный.
Нехватка гибкости.
– Некоторый уровень гибкости можно получить через аргументы pg_dump
, но они не слишком помогают если например нужно сменить тип ID.
– Как альтернативу можно рассмотреть написание SQL скрипта который будет сохранять в CSV. Если таблица достаточно проста - может сработать.
Вариант 2: SQL скрипт с экспортом и импортом через CSV
Формат CSV используется для обмена данных в системах 50 лет(с 1972). И имеет все шансы пережить первого человека на Марсе. Так что вполне вероятно поддерживается вашей СУБД.
В PostgreSQL выгружать в CSV и выгружать из него можно через команду COPY.
COPY "dbo.books" TO '/usr1/proj/bray/sql/all_the_books.csv' WITH DELIMITER ',' CSV HEADER;
COPY (SELECT * FROM "dbo.books") to '/usr1/proj/bray/sql/all_the_books.csv' WITH DELIMITER ',' CSV HEADER;
Минусы
Требует выгрузки на диск, что при использовании с серверами клиентов может создать проблем с допусками, пропускной способность и т.д.
Нужно писать SQL. Со всеми вытекающими.
Вариант 3: SQL скрипт с dblink
Команда dblink в PostgreSQL позволяет выполнить запрос в удалённой БД. Она позволяет переносить данные между двумя бд без промежуточных файлов. В сути своей - это вариант с классической связкой INSERT SELECT, но SELECT выполняется на другой БД.
Минусы
Да, это гибко, но растёт сложность SQL скрипта, а с ней и возможность выстрелить в себе ногу.
На БД клиентов сбоила при переносе БД более 15 ГБ за раз. Что б исправить эту проблему можно переносить данные через dblink по частям.
Варианты с использованием C# кода
Эти подходы требуют наличия контекста БД и для исходной БД и для БД получателя Простейший способ создания таковых в пункте "Создание контекста БД на основе существующей БД"
Вариант 1. dblink но с генерацией через код
Для использования команды требуется установить расширение через вызов SQL либо C# код который вызывает SQL.
Пример:
_destination.Database.ExecuteSqlRaw($"CREATE EXTENSION dblink");
Пример функции которая на основе существующего DbContext позволяет сформировать строку SQL запроса на перенос всех данных, либо с фильтром по полю yearofpublication, дополнительно отсортировав по этому полю.
Она принимает тип из контекста и сам контекст содержащий данный тип. Написана в рассчёте на то что данная таблица в исходной БД и получателе идентична по схеме.
public string GetEntityMigrationString(Type type, DbContext context, int? year = null)
{
var tableName = GetTableName(context, type);
var propertiesDictionary = GetTypeProperties(context, type);
var propertiesString = string.Join(", ", propertiesDictionary.Select(p => p.Key));
var propertiesWithTypesString = string.Join(", ", propertiesDictionary.Select(p => p.Value));
var sqlString =
$"INSERT INTO {tableName}({propertiesString}) SELECT {propertiesString} FROM dblink('{_sourceString}', 'SELECT {propertiesString} from {tableName}";
if (year != null)
{
sqlString +=
$" where yearofpublication >= {year} and yearofpublication <= {year} ORDER BY yearofpublication ASC";
}
sqlString +=
$"') AS x({propertiesWithTypesString})";
return sqlString;
}
private static string GetTableName(DbContext context, Type type)
{
var entityType = context.Model.FindEntityType(type);
var tableSchema = entityType.GetSchema();
var tableName = tableSchema == null
? entityType.GetTableName()
: tableSchema + '.' + entityType.GetTableName();
return tableName;
}
private static Dictionary<string, string> GetTypeProperties(DbContext context, Type type)
{
var entityType = context.Model.FindEntityType(type);
var propertiesDictionary = new Dictionary<string, string>();
foreach (var property in entityType.GetProperties())
{
var propertyColumnName = property.GetColumnName();
var propertyColumnNamePlusType = property.GetColumnName() + " " + property.GetColumnType();
propertiesDictionary.Add(propertyColumnName, propertyColumnNamePlusType);
}
return propertiesDictionary;
}
Эту строку можно вызвать в БД через ExecuteSqlRaw.
public void MigrateTable(Type type, DbContext referenceContext)
{
var tableName = GetTableName(referenceContext, type);
var dataMigrationString = GetEntityMigrationString(type, referenceContext);
Console.WriteLine($"Started importing {tableName} at {DateTime.Now:h:mm:ss tt}");
_destination.Database.ExecuteSqlRaw(dataMigrationString);
Console.WriteLine($"Imported {tableName} at {DateTime.Now:h:mm:ss tt}");
}
Минусы
Это гибридный подход и всё ещё нужен SQL.
Контекст БД содержит данные по схеме для классов. C# позволяет использовать это и засыпать синтаксическим сахаром и типизацией сверху. Например можно передавать поля классов как аргументы в функции и вытягивать их схему из контекста. Эта особенность и ряд других позволяют упростить написание, но далеко не всегда решить проблему с использованием SQL.На БД клиентов сбоила при переносе БД более 15 гб за раз. Что б исправить эту проблему можно переносить данные через dblink по частям.
Референсы: Официальная документация PostgreSQL по dblink
Вариант 2. Перенос средствами EF Core
Этот подход максимально опирается на EF Core.
Пример переноса небольшой таблицы Books с изменением типа Id.
public static class BooksMigration
{
public static async Task<Dictionary<int, Guid>> MigrateBooks(ContextsOptions contextsOptions)
{
var changedIds = new Dictionary<int, Guid>();
await using (var destinationContext = new DestinationContext(contextsOptions.DestinationDatabaseContextOptions))
{
await using (var legacyContext = new SourceContext(contextsOptions.SourceDatabaseContextOptions))
{
legacyContext.Books.AsNoTracking().ToList().ForEach(sourceBook =>
{
if (legacyContext == null || destinationContext == null)
{
throw new ArgumentNullException();
}
var destinationBook = InsertBookToDestination(destinationContext, sourceBook);
Console.WriteLine($"{sourceBook.Globalname} transferred, new id: {destinationBook.Id}(was {sourceBook.Id})");
changedIds.Add(sourceBook.Id, destinationBook.Id);
});
}
await destinationContext.SaveChangesAsync();
}
return changedIds;
}
private static Book InsertBookToDestination(DestinationContext destinationContext, Book sourceBook)
{
return destinationContext.Books.Add(new Book
{
Name = sourceBook.Name,
AuthorId = sourceBook.AuthorId,
Code = sourceBook.Code
}).Entity;
}
Используемый в коде ContextsOptions
public class ContextsOptions
{
public DbContextOptions<SourceContext> SourceDatabaseContextOptions { get; }
public DbContextOptions<DestinationContext> DestinationDatabaseContextOptions { get; }
public ContextsOptions(DbContextOptions<SourceContext> sourceDatabaseContextOptions, DbContextOptions<DestinationContext> destinationDatabaseContextOptions)
{
SourceDatabaseContextOptions = sourceDatabaseContextOptions;
DestinationDatabaseContextOptions = destinationDatabaseContextOptions;
}
public static ContextsOptions GetContextsOptions()
{
const string configPath = "appsettings.json";
var config = new ConfigurationBuilder()
.AddJsonFile(configPath)
.Build();
var sourceDatabase = config.GetConnectionString("SourceDatabase");
var destinationDatabase = config.GetConnectionString("DestinationDatabase");
var sourceDatabaseContextOptions = new DbContextOptionsBuilder<SourceContext>()
.UseNpgsql(sourceDatabase)
.Options;
var destinationDatabaseContextOptions = new DbContextOptionsBuilder<DestinationContext>()
.UseNpgsql(destinationDatabase)
.Options;
return new ContextsOptions(sourceDatabaseContextOptions, destinationDatabaseContextOptions);
}
}
Минусы
Этот метод загружает данные в память, что ресурсоёмко. Смягчается разделением блоки фиксированной величины с последовательной обработкой.
by design™ EF Core отслеживает изменения в моделях, что б упростить сохранение изменений в БД. Это уменьшает производительность чтения если нет намерения изменить загруженные данные.
Решается использованием AsNoTracking() и его альтернативой AsNoTrackingWithIdentityResolution(), доступной с EF Core 5+.EF Core не спроектирован для вставки больших объёмов записей в БД за запрос. Если требуется перенести большую таблицу то нужно найти способ делать bulk insert. Bulk insert функциональность не включена в EF Core и даёт разительное увеличение скорости переноса данных.
- Можно использовать Bulk Insert из платного Entity Framework Extensions (есть пробный период)
- Вот несколько вариантов для Npgsql провайдера используемого для работы с PostgreSQL. Бесплатно.
Нерабочие варианты
Вариант 1: Insert + Select
Так как это две разных БД, то простой insert + select не работает.
INSERT INTO TDestination (Id, Name)
SELECT Id, Name FROM TOrigin
WHERE Name ='John';
Вариант 2: Простой перенос средствами EF Core
EF Core не рассчитан на вставку большого количества записей за раз by design™. Такой запрос будет по меньшей мере не эффективен. Но для небольших таблиц пойдёт.
public void StraightforwardMigration(SourceContext sourceContext, DestinationContext destination)
{
destination.AddRange(sourceContext.Files);
}
Создание контекста БД на основе существующей БД
Этот пункт нужен если у вас нет контекста БД для EF Core. Для этой задачи отлично подходит Scaffold. Это функция EF Core позволяет создать контекст на основе существующей БД.
Основные шаги для уже существующего solution:
Создайте проект библиотеки классов в вашем Solution
Добавьте его в зависимости на него в вашем Startup проекте
Добавьте в него пакет Microsoft.EntityFrameworkCore.Design
Создайте контекст через вызов Scaffold команды.
dotnet ef dbcontext scaffold "Host=localhost;Port=5432;Database=SourceDatabase;Username=Superuser;Password=Superuser" Npgsql.EntityFrameworkCore.PostgreSQL -Context SourceScaffoldContext -OutputDir SourceScaffold
Visual studio
Scaffold-DbContext "Host=localhost;Port=5432;Database=SourceDatabase;Username=Superuser;Password=Superuser" Npgsql.EntityFrameworkCore.PostgreSQL -Context SourceScaffoldContext -OutputDir SourceScaffold
В данном примере используется PostgreSQL, но нет никакой проблемы использовать SqlServer, Cosmos, MySql или другую БД на которую существует EF Core провайдер. Список провайдеров
Наблюдения
Созданный Scaffold контекст организован не очень эффективно, читабельно, чисто, но для одноразового решения замечательно подходит. Если вы мигрируете на EF Core - может стать неплохой отправной точкой.
В PostgreSQL валидация по внешним ключам отключается через
ALTER TABLE {table} DISABLE TRIGGER ALL
но, это работает не для всех клиентов. Так что по возможности лучше обойтись без выключения этой валидации.
Выводы
Как я и говорил в начале материала - перенос данных между двумя БД в рамках миграции, далеко не самая лёгкая задача. Для миграции на своём проекте я сочетал варианты, что б использовать лучшее от каждого.
В процессе поиска этих вариантов я пробовал и другие. Например написание SQL скриптов, я отказался от него в пользу переноса данных и генерации SQL скриптов с помощью EF Core, в силу того в какую проблему превращалось поддержание и проверка SQL скриптов.
Комментарии (13)
akhkmed
04.09.2022 15:16+2Нерабочие варианты
Вариант 1: Insert + SelectЭто рабочий вариант в postgresql. Достаточно таблицу источника подключить к БД приёмника (не наоборот) через FDW. И это один из самых эффективных способов. К тому же, на нём можно сделать захват изменений и не потерять тех данных, что сделали в источнике во время копирования.
Depact Автор
04.09.2022 16:36Спасибо за идею. Не знал про это расширение.
Ради таких комментариев я опубликовал свою первую статью на хабре. Буду надеяться что во-первых удачную, а во-вторых не последнюю.
Из того, что я могу понять на данный момент - оно альтернатива вариантам с dblink, которые я выделил в отдельные варианты.
Поправьте меня если я не прав.
FDW более производителен
На FDW можно сделать read only доступ для выгрузки данных. Но у него есть
updatable
опцияdblink подключение существует в рамках сессии в которой оно создано, в то время как fdw пермаментен
dblink доступен на более ранних версиях PostgreSQL
Что делает его более выигрышной альтернативой dblink. Мне было бы интересно услышать ваше сравнение FDW и dblink подкреплённое реальным опытом, в силу того что я не имел опыта с FDW.
akhkmed
05.09.2022 00:22По п. 1, 2, 4 правильно. В 3 тоже, но надо добавить, что активное подключение сохраняется для повторного использования до конца сессии (keep_connections). Сравнение не проведу, мало использовал dblink. Но postgres_fdw точно надёжный даже в высоконагруженных базах, и он продолжает развиваться.
beduin01
04.09.2022 16:06А можете рассказать как правильно мигрировать на новые версии Субд когда сама БД весит к много и в нее постоянно идет запись)
Depact Автор
04.09.2022 16:45Могу ли я уточнить, что есть много? Пол сотни гигабайт? Пол террабайта? Террабайты? Или более?
Мне самому было услышать ваше решение этой проблемы, ибо исходное описание совпадает с тем что на моём проекте и я использовал методы представленные в моём материале.
Я опасаюсь что у меня не будет хорошего решения для вас. Это был мой первый раз.
Я видел, что пока я готовил мой материал к выходу, вышел цикл статей с сходной тематикой от ppetrov91. Полагаю у него должно быть куда больше компетенции в данном вопросе.
akhkmed
05.09.2022 00:26+1Силами самой СУБД postgres: pg_upgrade с параметром -k, будет небольшой перерыв, конечно.
Satyricon
04.09.2022 19:46+2Переносил данные с MS SQL на Postgres. Сначала через ODBC драйвер постргеса с помощью MS SQL Studio. 45 миллионов строк вставлялось примерно 4 часа. Каждая запись вставлялась отдельным инсертом, поэтому так долго. А это только одна таблица, там еще несколько таблиц по 3-4 миллиарда записей. В итоге переносил кусками через CSV. Правда одна таблица с комментариями содержала переносы строк в ячейке. Постгрес такого издевательства не смог пережить при импорте.
Evengard
05.09.2022 01:12Что делать, если кроме данных надо перегнать ещё кучу хранимок... Переписывать их все состариться можно.
akhkmed
05.09.2022 10:38+1Понимаю Ваше страдание, я переписывал руками, заодно оптимизировал кучу запросов и понял, что в mssql оптимизатор просто конфетка по отношению к самым ужасным запросам, которые без переписывания работают очень плохо в postgres.
Green21
С помощью Npgsql и его штатных средст (NpgsqlDataReader, NpgsqlDataAdapter, CreateInsertMethod(), Prepare()) можно тоже добиться неплохой скорости. А если распараллелить на несколько потоков - можно таблицы с несколькими млн данных перегнать за приемлемое время.
Depact Автор
Спасибо за интересную идею. В своём материале я дал ссылку на этот github коммент, содержащий ряд методов: https://github.com/npgsql/npgsql/issues/2779#issuecomment-573439342
Используя их я получил значительный прирост скорости. Могу ли я запросить хорошие примеры использования этих средств? Было бы интересно сравнить их в их лучшем виде с упомянутыми.
Green21
Спасибо за ссылку. Очень полезная. В принципе, я также использовал InsertPrepared и результат на 100000 почти тот же что и в описании из твоей ссылки.
Вот как это делал я: https://github.com/greenDev7/PGMigrationTest/blob/main/Form1.cs
У меня в таблице 14 столбцов. Однако, если данных будет порядка нескольких миллионов, то нужно уже чот выдумывать. Пока спасают pg_dump и pg_restore ))