PunchInsecurity: Tinubu to boost Air Force with five new helicoptersEl ComercioElecciones en Israel sin ganador claro: las opciones de Netanyahu para seguir en el poder y el voto árabeInquirerArmy kills 2 NPA rebels, seizes arms in separate Eastern Visayas opsBollywood HungamaPVR INOX sees record Q2 as Indian box office crosses Rs 4,000 crore for FIRST time in a quarter!The Jerusalem PostBen-Gvir calls for arrest of Arab party heads amid Qatar-linked election interference claimsZDF heuteAktuelle Pressemitteilungen des ZDFWirtualna PolskaWszystkich Świętych z szacunkiem dla natury. Zrezygnujcie z plastikowych dekoracji na grobyThe South AfricanBulls captain cleared of foul playRai NewsMosca: "Abbattuti 1214 droni ucraini in una notte." Drone russo su Dnipro uccide 13 personeالشرقانتخابات إسرائيل.. نتنياهو يتقدم دون حسم و19 مقعداً عربياً في حسابات الحكومة المقبلةGolem.deAnzeige: Aqara Zigbee Tür- und Fenstersensoren im 3er-Pack unter 40 Euro bei AmazonTimes of India EntertainmentAshwiny’s emotional note: Filmmaker pens magic happens post
The Daily Newsstand · Free, Always
Thursday, October 29, 2026

Поддержка нескольких реляционных СУБД в одном проекте: PostgreSQL и MSSQL

Translate

iLocks Online (в реестре российского ПО № 21482) - программно-аппаратный комплекс для управления безопасностью и контролем доступа в гостиницах, отелях и апартаментах. Система объединяет работу с электронными замками, пользователями, объектами размещения, правами доступа и событиями безопасности. Продукт ориентирован на корпоративный рынок и разворачивается в инфраструктуре конкретного заказчика. Это важное отличие от SaaS-решений, где разработчик заранее контролирует окружение и может самостоятельно выбрать единый технологический стек для всех клиентов.

В B2B-проектах ситуация обычно сложнее. У клиента уже могут быть собственные стандарты информационной безопасности, требования к размещению данных, внутренняя IT-команда и набор разрешённых технологий. В одной организации PostgreSQL используется как основная СУБД, в другой уже много лет эксплуатируется MSSQL. Если у клиента есть лицензия на MSSQL и специалисты, которые умеют его администрировать, переход на другую СУБД может быть для него нежелателен .Поэтому в iLocks Online PostgreSQL используется как СУБД по умолчанию, но при необходимости система может быть развёрнута на MSSQL. Поддержка двух СУБД позволяет гибче проходить технические согласования и учитывать существующую инфраструктуру заказчика. При этом у такого решения есть обратная сторона: поддержка нескольких баз данных добавляет проекту сложности и увеличивает стоимость разработки и сопровождения.

Дополнительная нагрузка появляется в нескольких областях:

  • конфигурация подключения;

  • создание и обновление схемы базы данных;

  • миграции;

  • типы данных;

  • последовательности;

  • интеграционные тесты;

  • диагностика проблем в разных окружениях.

Поэтому поддержка двух и более СУБД оправдана только тогда, когда за ней стоит явная бизнес-потребность. В нашем случае такая потребность связана с особенностями B2B-развёртывания и инфраструктурой клиентов.

Выбор провайдера базы данных

В проекте используется Entity Framework Core. Для каждой СУБД подключается собственный пакет:

  • Npgsql.EntityFrameworkCore.PostgreSQL — для PostgreSQL;

  • Microsoft.EntityFrameworkCore.SqlServer — для MSSQL.

Выбор СУБД выполняется через конфигурацию приложения. Например, в appsettings.json можно хранить тип базы данных и строку подключения:

{
  "Database": {
    "Provider": "PostgreSql",
    "ConnectionString": "Host=localhost;Port=5432;Database=ilocks;Username=postgres;Password=password"
  }
}

Для MSSQL конфигурация будет выглядеть иначе:

{
  "Database": {
    "Provider": "SqlServer",
    "ConnectionString": "Server=localhost;Database=ilocks;Trusted_Connection=True;TrustServerCertificate=True"
  }
}

Тип базы данных описывается перечислением:

public enum DbProvider
{
    PostgreSql,
    SqlServer
}

Настройки подключения можно представить следующим классом:

public sealed class DatabaseOptions
{
    public DbProvider Provider { get; set; }

    public string ConnectionString { get; set; } = string.Empty;
}

При запуске приложения нужная СУБД регистрируется в зависимости от настроек:

public static IServiceCollection AddApplicationDatabase(
    this IServiceCollection services,
    IConfiguration configuration)
{
    var databaseOptions = configuration
        .GetSection("Database")
        .Get<DatabaseOptions>()
        ?? throw new InvalidOperationException(
            "Database configuration is missing.");

    services.AddSingleton(databaseOptions);

    services.AddDbContext<ApplicationDbContext>(options =>
    {
        switch (databaseOptions.Provider)
        {
            case DbProvider.PostgreSql:
                options.UseNpgsql(databaseOptions.ConnectionString);
                break;

            case DbProvider.SqlServer:
                options.UseSqlServer(databaseOptions.ConnectionString);
                break;
        }
    });

    return services;
}

При таком подходе прикладной код не должен знать, какая именно СУБД используется в конкретной инсталляции. Сервисы и обработчики работают с одним ApplicationDbContext, а конкретный пакет доступа к базе выбирается на этапе запуска приложения.

Это позволяет сохранить общими:

  • бизнес-логику;

  • сервисы;

  • репозитории;

  • большую часть конфигурации сущностей;

  • LINQ-запросы;

  • логику заполнения справочников.

Различия остаются только там, где они действительно связаны с особенностями конкретной СУБД.

Миграции и начальное заполнение базы

Общая логика применения миграций может выглядеть одинаково для обеих СУБД:

public static async Task InitializeDatabaseAsync(
    IServiceProvider serviceProvider,
    CancellationToken cancellationToken = default)
{
    await using var scope = serviceProvider
        .CreateAsyncScope();

    var dbContext = scope.ServiceProvider
        .GetRequiredService<ApplicationDbContext>();

    await dbContext.Database.MigrateAsync(cancellationToken);

    await SeedDataAsync(dbContext, cancellationToken);
}

При этом для PostgreSQL и MSSQL создаются отдельные наборы миграций. Миграция, сгенерированная для одной СУБД, не является универсальной и не должна рассматриваться как миграция для другой.

EF Core учитывает выбранный пакет доступа к базе при генерации миграций. В результате SQL и операции изменения схемы будут соответствовать конкретной СУБД. Поэтому миграции для PostgreSQL и MSSQL необходимо создавать, хранить и проверять отдельно.

Общей при этом остаётся логика запуска миграций и первоначального заполнения базы:

await dbContext.Database.MigrateAsync(cancellationToken);
await SeedDataAsync(dbContext, cancellationToken);

Почему для этого проекта выбран Entity Framework Core

Главная причина использования Entity Framework Core — возможность описывать большую часть работы с данными на уровне общей модели и LINQ, не привязывая прикладной код к синтаксису конкретной СУБД.

У MSSQL используется собственный диалект SQL — T-SQL, или Transact-SQL. Он расширяет стандартный SQL конструкциями и функциями, специфичными для платформы Microsoft.

У PostgreSQL также есть собственный диалект SQL — обычно его называют диалектом SQL PostgreSQL. Кроме того, PostgreSQL поддерживает процедурный язык PL/pgSQL, который используется, например, для функций и хранимых процедур.

Различия между СУБД проявляются не только в названии диалекта. Отличаться могут: синтаксис ограничения количества записей, функции работы со строками и датами, выражения для генерации идентификаторов, типы данных, поведение NULL, именование объектов, синтаксис изменения схемы, особенности блокировок и конкурентного доступа.

Например, один и тот же запрос с ограничением количества записей может выглядеть по-разному:

-- MSSQL
SELECT TOP (@limit)
    *
FROM Locks
WHERE IsActive = 1
ORDER BY Name;
-- PostgreSQL
SELECT
    *
FROM "Locks"
WHERE "IsActive" = TRUE
ORDER BY "Name"
LIMIT @limit;

В прикладном коде этот сценарий можно описать единым LINQ-запросом:

var activeLocks = await dbContext.Locks
    .Where(lockEntity => lockEntity.IsActive)
    .OrderBy(lockEntity => lockEntity.Name)
    .Take(limit)
    .ToListAsync(cancellationToken);

EF Core преобразует LINQ-выражение в SQL с учётом выбранной СУБД. Такой подход позволяет убрать из прикладного кода большую часть различий между базами. Нам не нужно самостоятельно поддерживать две реализации каждого стандартного сценария работы с данными. Если же писать запросы вручную для каждой СУБД, со временем появляются несколько рисков.

Во-первых, реализации могут начать расходиться. В один запрос добавляется новое условие, сортировка или ограничение, а в другой это изменение забывается.

Во-вторых, исправление ошибки может попасть только в одну версию запроса. Тогда проблема будет проявляться исключительно у клиентов, использующих определённую СУБД.

В-третьих, усложняется тестирование. Вместо проверки одной бизнес-операции команде приходится поддерживать несколько реализаций одного и того же сценария.

Поэтому, если проект изначально должен работать с двумя или более СУБД, большую часть прикладных запросов следует писать на LINQ. Это уменьшает дублирование, снижает вероятность расхождения поведения и делает рефакторинг безопаснее. При этом задача не состоит в том, чтобы полностью запретить SQL. В отдельных местах он может быть необходим или удобнее LINQ. Важно другое: не допускать случайного распространения кода, зависящего от конкретной СУБД, по всему приложению. Если SQL-код всё же нужен, его следует изолировать и отдельно проверять для соответствующей базы.

С какими сложностями пришлось работать

Проверка миграций для разных СУБД

Для PostgreSQL и MSSQL создаются разные наборы миграций. Каждая из них должна проверяться отдельно на той СУБД, для которой была сгенерирована. Недостаточно убедиться, что миграция успешно создалась в проекте. Необходимо проверить, что она:

  1. применяется к пустой базе;

  2. корректно обновляет существующую базу;

  3. работает после последовательного применения предыдущих миграций;

  4. не приводит к потере данных;

  5. создаёт структуру, соответствующую ожиданиям приложения;

  6. учитывает особенности конкретной СУБД.

Особенно внимательно приходится проверять изменение типов колонок, добавление значений по умолчанию, переименование таблиц и колонок, создание последовательностей, изменение ограничений, а также миграции, связанные с датами и временем. Для каждой СУБД необходимо иметь возможность развернуть базу с нуля, применить полный набор миграций и проверить работу основных сценариев приложения.

Пользовательские sequence (последовательности)

В iLocks Online используются пользовательские sequence (последовательности). Они применяются, например, для генерации идентификаторов сущностей. Проблема заключается в том, что синтаксис использования sequence в PostgreSQL и MSSQL отличается. Для PostgreSQL значение по умолчанию можно задать следующим образом:

builder.Property(entity => entity.Id)
    .HasDefaultValueSql(
        "nextval('\"seqTestName\"')");

Для MSSQL используется другой синтаксис:

builder.Property(entity => entity.Id)
    .HasDefaultValueSql(
        "(NEXT VALUE FOR [seqTestName])");

Чтобы не смешивать эти различия с общей конфигурацией сущности, настройку можно применять отдельно в OnModelCreating:

protected override void OnModelCreating(
    ModelBuilder modelBuilder)
{
    modelBuilder.HasSequence("seqTestName");
    base.OnModelCreating(modelBuilder);

    var entityBuilder = modelBuilder.Entity<TestEntity>();

    entityBuilder.ToTable("TestEntities");

    entityBuilder.HasKey(entity => entity.Id);

    entityBuilder.Property(entity => entity.Id)
        .ValueGeneratedOnAdd();

    if (Database.IsNpgsql())
    {
        entityBuilder.Property(entity => entity.Id)
            .HasDefaultValueSql(
                "nextval('\"seqTestName\"')");
    }
    else if (Database.IsSqlServer())
    {
        entityBuilder.Property(entity => entity.Id)
            .HasDefaultValueSql(
                "(NEXT VALUE FOR [seqTestName])");
    }
}

Общая конфигурация сущности остаётся единой, а различается только SQL-выражение, используемое для генерации идентификатора. Важно проверять не только создание миграции, но и фактическую вставку данных в обе базы. Sequence должна корректно создаваться, использоваться и продолжать работу после обновления схемы.

Различия в типах данных

EF Core предоставляет общий API для настройки типов, но поведение самих СУБД всё равно может отличаться. На практике особенно внимательно нужно работать со следующими типами:

  • DateTime (UTC или местное время);

  • Guid (поддержка UUIDv4, UUIDv7);

  • строковыми типами (максимальной и минимальной длиной);

  • JSON-данными (JSON в MSSQL, JSONB в Postgres);

  • числовыми типами, если для них важна точность.

Для дат необходимо заранее определить, в каком виде они хранятся в системе: в UTC, в локальном времени или вместе с часовым смещением. Если это решение не зафиксировано, настройки окружения и особенности СУБД могут привести к разному поведению приложения.

Например, для времени создания сущностей используется единый подход:

builder.Property(entity => entity.CreatedAt)
    .IsRequired();

Но одной настройки модели недостаточно. Важно, чтобы приложение одинаково формировало и интерпретировало это значение независимо от выбранной базы.

Регистрозависимость и имена объектов

Чтобы схема была предсказуемой, команда явно задаёт имена таблиц и колонок, использует единый стиль именования, не полагается на автоматическое преобразование регистра, избегает имён, которые могут быть ключевыми словами одной из СУБД, и проверяет запросы на обеих базах.

Например:

builder.ToTable("AccessEvents");

builder.Property(entity => entity.CreatedAt)
    .HasColumnName("CreatedAt");

Такая конфигурация не устраняет все различия между СУБД, но снижает зависимость от настроек конкретного окружения.

Как проверяется совместимость с двумя СУБД

Подключение двух пакетов само по себе ещё не означает полноценную совместимость приложения с двумя базами данных. Необходимо проверять весь жизненный цикл каждой поддерживаемой СУБД.

Важные сценарии включают:

  1.  создание базы с нуля;

  2. применение полного набора миграций;

  3. обновление существующей базы;

  4. повторный запуск миграций;

  5. заполнение справочников;

  6. создание и изменение основных сущностей;

  7. фильтрацию и сортировку;

  8. пагинацию;

  9. работу с датами;

  10. операции с sequence;

  11. конкурентные изменения.

Ключевые интеграционные тесты запускаются отдельно на PostgreSQL и MSSQL. Для этого используются реальные экземпляры баз данных, а не только заглушки или in-memory-провайдеры.

Сценарии, зависящие от конкретной SQL-функции, структуры индекса или особенностей выбранной СУБД, также необходимо проверять интеграционными тестами. Это позволяет убедиться не только в корректности кода приложения, но и в фактическом взаимодействии с базой данных: проверить, какой SQL генерирует EF Core, как применяются отдельные миграции и создаются последовательности, одинаково ли работают основные операции и сохраняется ли целостность данных после изменений схемы.

Выводы

Поддержка PostgreSQL и MSSQL в iLocks Online появилась как ответ на конкретную бизнес-потребность. Продукт разворачивается в инфраструктуре заказчика, поэтому команда не всегда может выбирать СУБД самостоятельно. Возможность использовать уже имеющийся у клиента MSSQL упрощает внедрение системы и позволяет учитывать его корпоративные стандарты.

Чтобы сохранить единую кодовую базу, общую работу с данными необходимо строить на Entity Framework Core и LINQ. Различия между СУБД при этом не исчезают, а изолируются в отдельных наборах миграций, настройках типов данных, конфигурации sequence и других местах, где поведение PostgreSQL и MSSQL отличается.

Отдельное внимание нужно уделять не только запуску приложения, но и целостности данных. Поддержка двух СУБД должна означать, что операции создания и изменения схемы, обновления сущностей и заполнения справочников приводят к согласованному результату в каждой базе. Именно поэтому миграции и ключевые интеграционные сценарии необходимо проверять на реальных экземплярах обеих СУБД.

Поддержка нескольких СУБД оправдана тогда, когда она помогает решать реальные задачи продукта или бизнеса. Для iLocks Online это требование B2B-рынка и разнообразие инфраструктуры клиентов. Такой подход даёт заказчикам больше свободы при развёртывании системы, а продукту — возможность работать в организациях с разными корпоративными стандартами, сохраняя единую кодовую базу.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.