RE:NODE

Приложения10 мин чтения

.NET с PostgreSQL, MySQL или SQL Server: провайдеры

Как выбрать и подключить базу для приложения .NET: провайдеры EF Core, Npgsql, MySqlConnector и SqlClient, строки подключения, пулы и различия, на которых спотыкаются.

0 прочтений

.NET хорошо работает со всеми тремя. У SQL Server самые развитые инструменты и провайдер, который Microsoft пишет сама, у PostgreSQL есть Npgsql - отличный и не уступающий по скорости ничему в экосистеме, а у MySQL есть MySqlConnector под общественным провайдером Pomelo для Entity Framework Core. Выбор касается базы данных, а не драйвера: SQL Server Express бесплатен, но ограничен 10 GB на базу и примерно 1.4 GB памяти буфера; у PostgreSQL нет ограничений редакций и самый богатый набор возможностей; MySQL есть везде, и его легче всего запускать. Когда выбор сделан, проблемы доставляют одни и те же места для каждой: строка подключения, размер пула, поведение дат и строк и привязка миграций к конкретному провайдеру. Эта статья разбирает всё это бок о бок.

Провайдеры и драйверы#

У каждого стека баз данных в .NET два слоя. Внизу - драйвер ADO.NET, который говорит на сетевом протоколе базы и реализует DbConnection и DbCommand. Сверху, по желанию, - провайдер Entity Framework Core, который переводит LINQ в диалект SQL этой базы. Dapper и написанный вручную SQL используют драйвер напрямую.

База данныхДрайвер ADO.NETПровайдер EF CoreКто поддерживает
PostgreSQLNpgsqlNpgsql.EntityFrameworkCore.PostgreSQLПроект Npgsql
MySQLMySqlConnectorPomelo.EntityFrameworkCore.MySqlСообщество (Pomelo, MySqlConnector)
MySQL (альтернатива)MySql.DataMySql.EntityFrameworkCoreOracle
SQL ServerMicrosoft.Data.SqlClientMicrosoft.EntityFrameworkCore.SqlServerMicrosoft

Несколько замечаний к этой таблице:

  • `System.Data.SqlClient` - старый драйвер SQL Server, и он устарел. Новый код и все текущие версии EF Core используют Microsoft.Data.SqlClient. Скопированный пример со старым пространством имён по-прежнему компилируется и даёт вам драйвер со старыми значениями по умолчанию и без новых исправлений.
  • Для MySQL предпочитайте MySqlConnector. Он под лицензией MIT, по-настоящему асинхронный (MySql.Data от Oracle исторически реализовывал свои async-методы синхронно), и именно его использует Pomelo. Пакеты Oracle распространяются под GPL с исключением FOSS, что для некоторых коммерческих проектов имеет значение.
  • Релизы Pomelo следуют за мажорными версиями EF Core, иногда с отставанием в несколько месяцев. Прежде чем обновлять EF Core до новой мажорной версии, проверьте, что соответствующий релиз Pomelo существует. Npgsql обычно выпускал свой провайдер EF Core близко к релизу Microsoft.

Подключение каждой базы через EF Core#

Регистрация выглядит почти одинаково для всех трёх - в этом и смысл EF Core:

csharp
// PostgreSQLbuilder.Services.AddDbContext<AppDbContext>(o =>    o.UseNpgsql(builder.Configuration.GetConnectionString("Default")));// MySQL via Pomelovar mysql = builder.Configuration.GetConnectionString("Default");builder.Services.AddDbContext<AppDbContext>(o =>    o.UseMySql(mysql, ServerVersion.AutoDetect(mysql)));// SQL Serverbuilder.Services.AddDbContext<AppDbContext>(o =>    o.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

Pomelo нужна версия сервера, потому что MySQL и MariaDB различаются в том, какой SQL принимают. ServerVersion.AutoDetect при запуске открывает соединение, чтобы это узнать, и если база в этот момент недоступна, запуск приложения проваливается; new MySqlServerVersion(new Version(8, 4, 0)) обходится без этого обращения и в продакшене лучше.

Для Npgsql начиная с версии 7 рекомендуется один раз построить NpgsqlDataSource и передать его EF Core или использовать напрямую - именно там настраиваются сопоставления типов, enum и логирование:

csharp
var dataSource = new NpgsqlDataSourceBuilder(    builder.Configuration.GetConnectionString("Default")).Build();builder.Services.AddDbContext<AppDbContext>(o => o.UseNpgsql(dataSource));

GetConnectionString("Default") читает ConnectionStrings:Default из конфигурации, а в продакшене вы задаёте её переменной окружения с именем ConnectionStrings__Default. Строка подключения - это секрет; ей не место в appsettings.json в репозитории. Слои конфигурации разобраны в статье Конфигурация и секреты в ASP.NET Core.

Строки подключения бок о бок#

code
PostgreSQL (Npgsql)Host=db.example.net;Port=5432;Database=app;Username=app;Password=...;SSL Mode=PreferMySQL (MySqlConnector)Server=db.example.net;Port=3306;Database=app;User ID=app;Password=...;SslMode=PreferredSQL Server (Microsoft.Data.SqlClient)Server=tcp:db.example.net,1433;Database=app;User ID=app;Password=...;Encrypt=True;TrustServerCertificate=True

Различия, на которых спотыкаются:

  • SQL Server ставит порт после запятой, а не двоеточия, и отдельного ключевого слова Port у него нет. db.example.net:1433 читается как имя хоста и не работает. Та же запятая используется в поле имени сервера в SQL Server Management Studio.
  • SQL Server шифрует по умолчанию. Начиная с Microsoft.Data.SqlClient 4.0 (а значит, с EF Core 7) Encrypt по умолчанию равен True, и драйвер проверяет сертификат сервера. Сервер с самоподписанным сертификатом - а именно такой SQL Server генерирует себе сам, если никакой не настроен, - падает с ошибкой «The certificate chain was issued by an authority that is not trusted», пока вы не добавите TrustServerCertificate=True или не установите доверенный сертификат. Вариант этого для каждого драйвера разобран в статье Строки подключения SQL Server.
  • MySQL 8.4 аутентифицирует через `caching_sha2_password`. По незашифрованному соединению первому входу нужен публичный RSA-ключ сервера, и MySqlConnector отказывает с «Retrieval of the RSA public key is not enabled for insecure connections», если вы не используете TLS (SslMode=Required) или не добавите AllowPublicKeyRetrieval=True. Предпочитайте TLS.
  • В Npgsql по умолчанию `SSL Mode=Prefer`, то есть TLS используется, когда сервер его предлагает, без проверки сертификата. Используйте Require, чтобы отказываться от открытого текста, и VerifyFull с доверенным CA, когда сертификат нужно проверять.

Пароли, содержащие ; или =, в строках подключения ADO.NET нужно брать в кавычки - Password="a;b=c" - или собирать через класс ConnectionStringBuilder драйвера, который экранирует за вас.

Пулы соединений и таймауты#

Все три драйвера по умолчанию держат пул соединений - по одному пулу на каждую отдельную строку подключения в процессе. Открытие соединения берёт его из пула; dispose возвращает обратно. Поэтому стандартный паттерн - DbContext на запрос, уничтожаемый в конце, - стоит дёшево.

НастройкаNpgsqlMySqlConnectorSqlClient
Максимальный размер пулаMaximum Pool Size=100MaximumPoolSize=100Max Pool Size=100
Минимальный размер пулаMinimum Pool Size=0MinimumPoolSize=0Min Pool Size=0
Таймаут подключенияTimeout=15ConnectionTimeout=15Connect Timeout=15
Таймаут командыCommand Timeout=30DefaultCommandTimeout=3030 с (на команду)
Удаление простаивающих соединенийConnection Idle Lifetime=300ConnectionIdleTimeout=180Управляет драйвер

Значения в секундах. Сто соединений на процесс - намного больше, чем небольшой сервер баз данных может толково использовать. PostgreSQL в особенности тратит реальную память на каждое соединение, а у базы на тарифе с 1 или 2 GB max_connections заметно меньше, чем могли бы открыть три экземпляра приложения с пулами по умолчанию. Подбирайте пул под базу, а не наоборот: двадцати-тридцати соединений хватает большинству приложений на небольшом сервере, а приложение, которому нужно больше, обычно слишком долго держит соединения. Почему меньший пул часто быстрее, объясняет статья Пулы соединений и лимиты.

Когда пул исчерпан, запросы ждут таймаута подключения, а затем падают с ошибкой таймаута пула. Эта ошибка почти никогда не означает, что пул слишком мал. Она означает, что соединения утекают (не освобождаются) или удерживаются во время медленной работы - например, HTTP-вызова посреди транзакции.

У EF Core есть собственный, отдельный пул: AddDbContextPool<AppDbContext>() переиспользует экземпляры DbContext (по умолчанию до 1024), экономя на аллокациях. Он не меняет количество соединений с базой и требует, чтобы ваш контекст не хранил состояние конкретного запроса.

Повторы и временные сбои#

Сети теряют пакеты, базы перезапускаются. EF Core умеет автоматически повторять неудавшиеся операции:

csharp
o.UseSqlServer(conn, sql => sql.EnableRetryOnFailure(    maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(10), errorNumbersToAdd: null));o.UseNpgsql(conn, pg => pg.EnableRetryOnFailure());o.UseMySql(conn, version, my => my.EnableRetryOnFailure());

При включённых повторах транзакция, которую вы открываете сами через BeginTransactionAsync, выбрасывает исключение, потому что EF Core не может воспроизвести половину транзакции. Вместо этого оберните всю единицу работы в execution strategy:

csharp
var strategy = db.Database.CreateExecutionStrategy();await strategy.ExecuteAsync(async () =>{    await using var tx = await db.Database.BeginTransactionAsync();    // ... several SaveChangesAsync calls    await tx.CommitAsync();});

Повторы скрывают кратковременные сбои. Базу, которая действительно лежит, они на время повторов превращают в очень медленное приложение, так что держите число попыток скромным и логируйте каждый повтор.

Различия, которые кусаются при переходе#

EF Core скрывает диалект SQL. Поведение базы он не скрывает, и вот с чем сталкиваются при переносе приложения с одного движка на другой или при тестах на SQLite с продакшеном на чём-то другом:

  • Сравнение строк и регистр. Collation по умолчанию в SQL Server (SQL_Latin1_General_CP1_CI_AS) и в MySQL 8 (utf8mb4_0900_ai_ci) сравнивают без учёта регистра, так что WHERE Email = 'Bob@x.com' находит bob@x.com. PostgreSQL сравнивает с учётом регистра. Поиск при входе, который работал на SQL Server, молча перестаёт находить пользователей на PostgreSQL. Нормализуйте email при записи или используйте регистронезависимый collation или тип citext в PostgreSQL.
  • Даты и часовые пояса. Npgsql 6 и новее сопоставляет DateTime с timestamp with time zone и отказывается записывать DateTime, у которого Kind не Utc, с ошибкой «Cannot write DateTime with Kind=Unspecified to PostgreSQL type 'timestamp with time zone'». Храните везде UTC или используйте DateTimeOffset. datetime2 в SQL Server и datetime в MySQL вообще не хранят часовой пояс, так что тот же код без возражений пишет местное время, а баг всплывает позже.
  • Unicode. В SQL Server EF Core сопоставляет string с nvarchar, то есть Unicode. Созданные вручную таблицы с колонками varchar могут терять символы, не входящие в кодовую страницу collation. В MySQL используйте utf8mb4 - старый псевдоним utf8 трёхбайтовый и не может хранить emoji. Подробнее - в статьях Collation и Unicode в SQL Server и utf8mb4 и collation в MySQL.
  • Длина строк по умолчанию. Свойство string без ограничения становится nvarchar(max) в SQL Server, longtext в MySQL и text в PostgreSQL. В SQL Server колонки nvarchar(max) не могут быть ключами индекса; задайте [MaxLength] свойствам, по которым будете фильтровать.
  • Значения identity. Все три генерируют ключи - через IDENTITY, AUTO_INCREMENT или identity-колонки на основе последовательностей. Пропуски после откатов и перезапусков нормальны во всех трёх; никогда не используйте ключ как счётчик.
  • Миграции привязаны к провайдеру. Миграция, сгенерированная для SQL Server, содержит типы колонок и аннотации SQL Server. Смена движка означает генерацию миграций с нуля для нового провайдера, а не повторное использование папки.

Выбор базы данных для приложения .NET#

По одним только возможностям любая из трёх потянет типичное веб-приложение. Выбор обычно сводится к ограничениям, инструментам и тому, что вы уже знаете.

SQL Server Express подходит командам, которые и так живут в SQL Server Management Studio, приложениям, использующим возможности, специфичные для SQL Server (temporal tables, хранимые процедуры на T-SQL, полный набор инструментов SQL Server), и всему, что позже может переехать на платную редакцию SQL Server или в Azure SQL. Его ограничения вполне реальны: каждая база ограничена 10 GB данных, buffer pool - примерно 1,410 MB, а вычисления - меньшим из одного сокета или четырёх ядер, и нет SQL Server Agent для задач по расписанию. Когда этого достаточно, разбирает статья Хостинг SQL Server Express.

PostgreSQL - рекомендация по умолчанию для нового проекта без других ограничений: никаких ограничений редакций, отличная поддержка JSON, богатая индексация, а Npgsql первоклассный. Первое подключение разобрано в статье Удалённые подключения к PostgreSQL.

MySQL - самый лёгкий в эксплуатации из трёх и знакомый почти всем. Это хороший выбор, когда приложение уже на него рассчитано или делит базу с софтом на PHP.

Более подробное сравнение, включая MongoDB и Valkey, - в статье Какую базу данных выбрать.

В RE:NODE каждая из них - отдельный тариф базы данных с учётными данными, генерируемыми для каждого сервера, и доступом по хосту и порту тарифа: PostgreSQL от $6 в месяц, MySQL 8.4 LTS от $4 с генерируемыми паролем root, базой данных приложения и пользователем приложения, и SQL Server 2022 Express от $6 с паролем sa и созданной базой, назначенной базой по умолчанию для sa. У тарифов баз данных нет слота прокси - приложение подключается прямо к хосту и порту. Тарифы приложений C# / .NET тоже включают собственные слоты баз данных, которые создаются в панели с генерируемыми хостом, пользователем и паролем.

Решение проблем#

`A network-related or instance-specific error occurred` (SQL Server). Неправильный хост или порт, двоеточие вместо запятой перед портом или firewall. Сначала проверьте порт.

`The certificate chain was issued by an authority that is not trusted` (SQL Server). Шифрование включено по умолчанию, а сертификат сервера самоподписанный. Добавьте TrustServerCertificate=True или установите сертификат от доверенного CA.

`Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool`. Соединения утекают или удерживаются слишком долго. Ищите контексты или соединения, созданные вне using, и работу, выполняемую внутри открытых транзакций.

`53300: sorry, too many clients already` (PostgreSQL) или `Too many connections` (MySQL). Пулы всех ваших экземпляров приложения вместе превышают лимит сервера. Уменьшите Maximum Pool Size.

`Unable to connect to any of the specified MySQL hosts`. Хост, порт или firewall. Если хост правильный, проверьте, разрешено ли пользователю подключаться с вашего адреса.

FAQ#

Entity Framework Core медленнее Dapper?

На простых запросах современный EF Core близок к нему, особенно с AsNoTracking() и проекциями. Dapper по-прежнему выигрывает по накладным расходам на маппинг и даёт точный контроль над SQL. Многие приложения используют EF Core для записи и миграций, а Dapper - для нескольких тяжёлых запросов на чтение, с одной и той же строкой подключения.

Можно ли использовать SQLite в тестах и SQL Server в продакшене?

Можно, но это будет скрывать баги: чувствительность к регистру, обработка дат, поведение транзакций и трансляция SQL - всё это различается. Запускайте интеграционные тесты на том же движке, что и продакшен, при необходимости в контейнере.

Какой провайдер MySQL использовать с EF Core?

Pomelo поверх MySqlConnector. Он самый распространённый и самый полный. Прежде чем обновлять EF Core, проверьте, что его релиз для вашей мажорной версии EF Core существует.

Нужен ли TLS между приложением и базой данных?

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

Сколько соединений должен разрешать пул?

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


Комментарии

Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.

0/2000