Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 23 из 25 ~18 мин +50 XP

Мультибазовость: диалекты — один код для Postgres, MySQL и SQLite в GORM

Прошлый [урок](/courses/gorm/lessons/22-kastomnye-tipy/) закрыл сериализацию, но дважды в нём прозвучало обещание: jsonb, GIN и оператор @> мы назвали «знанием диалекта», а text[] — Postgres-only, и разговор честно отложили. Сегодня долги отдаются: gorm диалекты — это разница между «работает у меня» и «работает у заказчика», и живёт она не в прикладном коде, а в объекте под названием Dialector. Ядро GORM одно, но SQL собирает и исполняет драйвер: у него свои цитаты, свои плейсхолдеры, свои хвосты RETURNING и своя карта ошибок. Разберём три официальных драйвера — PostgreSQL поверх pgx, MySQL поверх go-sql-driver, SQLite поверх go-sqlite3 — и их отличия по исходникам: clause-списки, DataTypeOf, ClauseBuilders, error_translator. Запрос gorm mysql postgres вбивают в поиск обычно в момент переезда — и переезд рушится не на SELECT-ах, а на мелочах: varchar против text, бэктики против двойных кавычек, 1062 против 23505. Отдельный раздел — SQLite in-memory: лаборатория без сервера, с ловушкой пула соединений и cache=shared. И ровно одна оговорка о формате: GORM требует живую СУБД, поэтому runner-ов в уроке нет — код не исполнить в песочнице, вас проверяют quiz, predict, fill, order и карточки, а SQL-следы рассчитаны на локальный Go с любой из трёх баз. Карта курса — на его [странице](/courses/gorm/).

Содержание урока · 16 разделов

Ядро и диалекты: кто на самом деле пишет SQL#

GORM разделена на ядро и драйверы, и это разделение — не украшение архитектуры, а точка, где принимается всё диалектное. Ядро собирает запрос не как строку, а как Statement со списком clause-ов: INSERT, VALUES, ON CONFLICT, WHERE, ORDER BY, LIMIT — и каждый clause умеет писать себя в буфер. Кто ведёт этот буфер в текст конкретной базы? Dialector. Это интерфейс из gorm.go с обязательными методами Name() (имя диалектора: postgres, mysql, sqlite), Initialize (поднять соединение и зарегистрировать колбэки), DataTypeOf (перевод типа поля модели в тип колонки), BindVarTo (как рисуется плейсхолдер), QuoteTo (как цитируются имена), Explain (красивый SQL-след для лога) — плюс необязательные расширения: ClauseBuilders (подмена поведения отдельных clause-ов) и Translate из интерфейса ErrorTranslator (нормализация ошибок, до неё дойдём в середине урока). Три официальных диалектора живут в трёх модулях: gorm.io/driver/postgres построен поверх pgx от Джека Кристенсена — того самого драйвера, чью семью pgtype мы упоминали в разговоре о массивах; gorm.io/driver/mysql — поверх классического go-sql-driver/mysql; gorm.io/driver/sqlite — поверх mattn/go-sqlite3, а это байндинг к C-библиотеке, то есть честное требование CGO. Каждый диалектор при Initialize дописывает свой набор колбэков: именно там PostgreSQL включает RETURNING, SQLite — своё, MySQL — своё. Отсюда рабочее правило урока: ваш прикладной код обращается к gorm.DB, диалектное — спрятано в Dialector, и перенос базы — это замена одного аргумента Open плюс аудит того, чем вы всё-таки воспользовались из знаний конкретной СУБД.

Канон: один код — три базы

GO
// Один и тот же прикладной код - меняется только первый аргумент gorm.Open.
import (
    "gorm.io/driver/mysql"
    "gorm.io/driver/postgres"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

switch kind {
case "postgres":
    // DSN из README драйвера: пары ключ=значение через пробел, поверх pgx
    dsnPG := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai"
    db, err = gorm.Open(postgres.Open(dsnPG), &gorm.Config{})
case "mysql":
    // DSN из README: порт внутри tcp(...), parseTime=True обязателен для time.Time
    dsnMy := "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local"
    db, err = gorm.Open(mysql.Open(dsnMy), &gorm.Config{})
case "sqlite":
    // SQLite - это файл, сервера нет; README: db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
    db, err = gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
}
// Дальше - код курса: db.Create, db.Where, db.First -
// работают во всех трёх мирах одинаково.

Три импорта диалекторов в одном файле допустимы: выбирает не код, а конфигурация. Формат DSN у каждой базы свой, разбор строк подключения — материал второго урока; здесь важно другое: после Open весь прикладной код одинаков, а диалектные отличия сидят внутри Dialector.

DSN-шпаргалка трёх драйверов#

ДиалекторПоверх чегоDSN из READMEНа что смотреть
gorm.io/driver/postgrespgx (jackc/pgx)host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghaiпары ключ=значение через пробел
gorm.io/driver/mysqlgo-sql-driver/mysqlgorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Localбез parseTime=True time.Time не отсканируется
gorm.io/driver/sqlitemattn/go-sqlite3 (нужен CGO)sqlite.Open("gorm.db") — просто путь к файлуможно file::memory:?cache=shared — база в памяти

Clause-конвейер: где диалектор вмешивается в SQL#

Чтобы спорить о диалектах предметно, посмотрим, чем диалекторы отличаются в исходниках. В ядре, в callbacks/callbacks.go, объявлены базовые clause-списки: createClauses = []string{"INSERT", "VALUES", "ON CONFLICT"}, updateClauses = []string{"UPDATE", "SET", "WHERE"}, deleteClauses = []string{"DELETE", "FROM", "WHERE"} — из них колбэки собирают каждый запрос. Диалектор получает право дописать или подменить этот список при регистрации колбэков, и вот что делают трое. PostgreSQL-диалектор в postgres.go добавляет "RETURNING" ко всем трём спискам: CreateClauses, UpdateClauses и DeleteClauses — то есть RETURNING есть не только у вставки, но и у UPDATE и DELETE, где Postgres умеет возвращать старые и новые значения колонок. SQLite-диалектор просто объявляет свои списки с RETURNING на борту: CreateClauses: []string{"INSERT", "VALUES", "ON CONFLICT", "RETURNING"}, а в UpdateClauses у него даже появляется FROM — SQLite с версии 3.33 умеет UPDATE ... FROM, и драйвер этим пользуется. MySQL-диалектор делает противоположное: он расширяет UpdateClauses и DeleteClauses парой "ORDER BY", "LIMIT" — записями вроде UpdateClauses = []string{"UPDATE", "SET", "WHERE", "ORDER BY", "LIMIT"} из mysql.go. Это не прихоть, а документированная способность MySQL: у него ORDER BY и LIMIT допустимы прямо в UPDATE и DELETE. Читая эти списки, вы читаете фактическую карту возможностей: что диалектор дописал — то база умеет, чего не дописал — того не обещано. Пятый урок уже показал, как clause.OnConflict превращается в SQL; сегодня смотрим на конвейер шире — как на таблицу отличий, записанную прямо в коде драйверов.

Проверка: что говорит расширение clause-списков

В mysql.go объявлено UpdateClauses = []string{"UPDATE", "SET", "WHERE", "ORDER BY", "LIMIT"}, тогда как в ядре GORM updateClauses = []string{"UPDATE", "SET", "WHERE"}. Что это сообщает про MySQL?

RETURNING против LastInsertId: возврат ключей#

Мост из пятого урока: ID свежевставленной строки возвращается в структуру двумя механизмами — урок о Create разбирал этот выбор с прикладной стороны, а здесь смотрим, кто его делает. Решает не вы, а диалектор. PostgreSQL добавляет RETURNING к clause-спискам всегда — строка postgres.go «callbackConfig.CreateClauses = append(callbackConfig.CreateClauses, "RETURNING")» — и заодно получает назад значения полей с default-выражениями: база прислала их в той же команде. SQLite-диалектор держит RETURNING в своих списках сразу: отдельная СУБД-версия не опрашивается, а сам SQLite поддерживает RETURNING начиная с 3.35, и современные байндинги его дают. MySQL — самая интересная история, потому что у классического MySQL RETURNING нет вовсе, и mysql.go включает его условно: в Initialize драйвер выполняет SELECT VERSION(), и только если в ответе обнаружена MariaDB версии 10.5 и новее — условие checkVersion(dialector.ServerVersion, "10.5") внутри ветки с "MariaDB" — к спискам добавляется RETURNING. Две оговорки исходников: опция DisableWithReturning: true отключает механизм принудительно, а SkipInitializeWithVersion: true пропускает запрос версии — и тогда withReturning остаётся false даже на MariaDB, молча. На классическом MySQL остаётся второй путь: GORM читает result.LastInsertId() у драйвера и раздаёт значения; на срезах — первый ключ плюс шаг автоинкремента. Практическая цена та же, что в пятом уроке: поле с default-выражением после Create на MySQL останется нулём в структуре — RETURNING читать некому, и никаких Translate-подмен это не лечит. Проверяйте версию — драйвер делает это за вас, но только если вы ему не запретили.

gorm диалекты: карта регистра, цитат и плейсхолдеров#

Второй слой отличий — то, что видно в каждой строке SQL-следа. Плейсхолдеры: метод BindVarTo у postgres-диалектора пишет '$' и номер — $1, $2, $3; у mysql- и sqlite-диалекторов он умещается в одну строку — writer.WriteByte('?'). Поэтому SQL-след с нумерованными долларами выдаёт PostgreSQL с pgx мгновенно. Цитирование имён: QuoteTo postgres-диалектора оборачивает идентификаторы в двойные кавычки — это стандарт SQL; mysql-диалектор пишет бэктики; а вот sqlite-диалектор GORM, вопреки ожиданию, тоже пишет бэктики — и это легально, потому что SQLite принимает бэктики ради совместимости с MySQL. Регистр: здесь GORM не помощник, он передаёт условие базе как есть — седьмой урок уже разбирал ILIKE как регистрозависимую компенсацию Like-а у Postgres, а это материал седьмого урока; MySQL при коллациях *_ci ищет без регистра сам, SQLite не различает регистр для ASCII. То же с датами и булевыми: timestamptz у Postgres, datetime с точностью у MySQL, у которого parseTime=True в DSN не опция, а условие чтения time.Time. Никакой магии в этих отличиях нет — каждое сидит в конкретном методе конкретного диалектора, и когда «на другом диалекте сломался поиск по подстроке», ответ ищите не в логах GORM, а в этой карте: чем пользователь воспользовался — ILIKE, бэктиками или коллацией — и есть ли аналог там, куда переезжают.

Проверка: узнать диалектор по следу

В SQL-следе вы видите WHERE slug = $1. Какой диалектор собрал запрос?

jsonb, json и просто текст: JSON на стороне базы#

Пришло время отдать долг прошлого урока: jsonb, GIN и оператор @> были названы знанием диалекта — теперь разбираем их целиком. PostgreSQL хранит jsonb в распарсенном бинарном виде: ключи дедуплицированы, порядок не сохраняется, зато доступны операторы — прежде всего @> «содержит», который проверяет вложенность документа. На jsonb строится GIN-индекс, и план запроса с WHERE data @> '{"city":"Пенза"}' показывает Bitmap Index Scan по GIN: выборка мгновенная, потому что индекс организован под поиск по элементам, а не по строке. Индексы — материал восемнадцатого урока: он показывал, как GORM строит их тегами, но GIN на jsonb-колонке — уже SQL-знание: CREATE INDEX ... USING gin (data), исполняемый миграцией или db.Exec. MySQL отвечает своим типом json (с версии 5.7.8): тоже распарсенное хранение, но оператора @> нет — поиск по элементу идёт функциями, а индекс строят функциональным: CREATE INDEX ... ((CAST(data->'$.city' AS CHAR(64)))) по выражению пути. SQLite — третий мир: «JSON-колонка» у него обычный TEXT, разбора на стороне хранилища нет, поиск — функциями вроде json_extract из семейства json1, а индекс, если нужен, — тоже по выражению. Отсюда честная иерархия для переезда: jsonb+GIN+@> — максимум скорости поиска и максимум привязки к диалекту; json с функциями — середина; текст с сериализацией на границе модели — медленный на фильтре, но переносимый везде. Один тег type:jsonb в модели — и вы уже в первом мире со всеми его преимуществами и ограничениями.

JSON-фильтр в трёх диалектах

GO
// Модель одна, колонка - по диалекту. type:jsonb даёт Postgres-тип.
type Meta struct {
    ID   uint
    Data string `gorm:"type:jsonb"` // сериализация структур - материал прошлого урока
}

// PostgreSQL: GIN-индекс и оператор @> "содержит"
db.Exec(`CREATE INDEX idx_meta_data ON metas USING gin (data)`)
db.Where("data @> ?", `{"city":"Пенза"}`).Find(&rows)
// план: Bitmap Index Scan по idx_meta_data

// MySQL: тип json, поиск функцией, индекс - функциональный по выражению
db.Where("data -> '$.city' = ?", "Пенза").Find(&rows)
db.Exec(`CREATE INDEX idx_city ON metas ((CAST(data->'$.city' AS CHAR(64))))`)

// SQLite: колонка текстовая, поиск - функцией json_extract
db.Where("json_extract(data, '$.city') = ?", "Пенза").Find(&rows)
// без индекса это полный проход; индекс по выражению помогает лишь частично

Прикладная форма запроса меняется от базы к базе: это уже не GORM-код, а SQL-знание, спрятанное в строках условий. GORM честно передаёт условие базе как есть — диалект-операторы в строках условий не переводятся. Тег type:jsonb на MySQL соберёт невалидный тип колонки — это ловушка ниже.

Прогноз: одна модель — три колонки

Прочитайте код и выберите, что он выведет:

type TodoItem struct {
    ID    uint
    Title string
    Tags  []string `gorm:"serializer:json"`
}
// AutoMigrate исполняется на PostgreSQL, MySQL и SQLite.
// Какой тип колонки tags получится в каждой из трёх баз?

DataTypeOf: как одна модель даёт три таблицы#

Прогноз выше опирается на метод, который стоит разобрать до строчек. DataTypeOf — это перевод типа поля модели в SQL-тип колонки, и у каждого диалектора он свой. Разберём три показательные ветки. Строка: postgres.go при Size 0 возвращает text, а при заданном размере varchar(n); mysql.go без размера берёт DefaultStringSize — конфиг-поле со значением 256 по умолчанию — и делает varchar(256), но в ветке есть ещё одна деталь с комментарием «// utf8mb4»: если поле ключевое, имеет default или под ним индекс, размер ужимается до 191 — классическое наследие ограничения длины индексного ключа в старых utf8mb4-индексах MySQL. SQLite пишет text всегда — в SQLite VARCHAR(255) это просто тип с аффинностью TEXT. Булев тип: Postgres даёт честный boolean, MySQL тоже пишет boolean, хотя хранит его как tinyint(1); sqlite-диалектор возвращает numeric — и таблица создаётся с numeric-колонкой, которую SQLite, благодаря аффинностям, держит как хочет. Автоинкремент: bigserial у Postgres, int auto_increment у MySQL, а sqlite.go возвращает целую формулу — «integer PRIMARY KEY AUTOINCREMENT». Время: timestamptz против datetime(3) — mysql-диалектор подставляет DefaultDatetimePrecision, равное трём знакам после запятой. Вывод для практики: миграция из четвёртого урока не «создаёт одну и ту же таблицу в разных базах» — она создаёт три разных таблицы из одной модели, и сравнение d в psql с SHOW CREATE TABLE в MySQL — лучший способ увидеть диалект вживую. Отсюда и правило: не полагайтесь на дефолтные типы там, где важна точность, — задавайте size и type явно.

Одна модель — три таблицы: переводы DataTypeOf#

Поле моделиPostgreSQLMySQLSQLite
Status boolbooleanboolean (хранится как tinyint(1))numeric
ID uint, автоинкрементbigserialint auto_incrementinteger PRIMARY KEY AUTOINCREMENT
Title string без sizetextvarchar(256), под индексом 191text
DoneAt time.Timetimestamptzdatetime(3)datetime
Payload []bytebyteablobblob

ON CONFLICT против ON DUPLICATE KEY#

Три режима clause.OnConflict из пятого урока вы уже знаете как готовый приём; сейчас интересна механика, как один clause живёт в разных базах. Имя clause-а всегда «ON CONFLICT» — его возвращает метод Name() структуры clause.OnConflict, и в clause-списках вставки у всех диалекторов стоит именно ON CONFLICT, унаследованный от ядра. Но перед записью в буфер каждый диалектор имеет право перехватить clause через ClauseBuilders — карту подмен, которую Initialize передаёт ядру. MySQL этим правом пользуется полноценно: в mysql.go карта содержит ClauseOnConflict, и её функция пишет в буфер не стандартную форму, а строку builder.WriteString("ON DUPLICATE KEY UPDATE ") — дальше идут присваивания, которые GORM собрала из полей clause-а: перечисленные колонки или обновление всех сразу. Дока формулирует совместимость дословно: GORM provides compatible Upsert support for different databases — совместимый upsert для разных баз, то есть API один, а SQL подставляется по диалекту. Самая неочевидная деталь — DoNothing на MySQL: у MySQL нет DO NOTHING, поэтому диалектор в той же функции превращает «ничего не делать» в фиктивное присваивание ключа самому себе — INSERT ... ON DUPLICATE KEY UPDATE id=id: строка-дубль найдена, ничего не изменено, RowsAffected ноль. Второе отличие — цель конфликта: PostgreSQL требует сказать, по какому ограничению конфликт, а MySQL цели не принимает вовсе — Columns игнорируются, потому что ON DUPLICATE KEY срабатывает по любому уникальному ключу. SQL Server, упомянутый в доке, решает задачу совсем другим statement-ом — MERGE INTO; но он за рамками тройки урока. Мораль: clause.OnConflict — это абстракция над тремя разными SQL-мирами, и при отладке смотрите итоговый SQL, а не API.

Проверка: DoNothing на MySQL

db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user) исполняется на MySQL. Что уйдёт в SQL?

Ошибки дублей: 1062, 23505 и нормализация#

Пятый урок показал, что вставка с уникальным Slug рано или поздно упрётся в констрейнт, и назвал коды: 23505 у Postgres, 1062 у MySQL. Теперь смотрим, как с этим работать на трёх базах одним кодом. Без помощи GORM вы классифицируете тексты и типы драйверов: доки советуют switch по mysql.MySQLError с веткой case 1062 — «MySQL code for duplicate entry»; у Postgres ошибка — pgconn.PgError с полем Code; SQLite отвечает текстом «UNIQUE constraint failed», а численно — расширенными кодами 1555 и 2067 (нарушение первичного ключа и уникального индекса). Три базы — три разных типа — три switch-а в вашем коде. GORM предлагает второй путь: в errors.go объявлены gorm.ErrDuplicatedKey = errors.New("duplicated key not allowed") и gorm.ErrForeignKeyViolated, а конфиг gorm.Open принимает TranslateError: true — доки объясняют: когда TranslateError включён, GORM converts database-specific errors into its own generalized errors — и добавляют: By enabling TranslateError, GORM provides a more unified way of handling errors across different databases. Механика честно локализована в драйверах: у каждого свой файл error_translator.go с картой кодов — postgres.go-фамилия кодов: «23505»: gorm.ErrDuplicatedKey, «23503»: ErrForeignKeyViolated, «42703»: ErrInvalidField, «23514»: check-constraint; mysql-карта — 1062: gorm.ErrDuplicatedKey и пара 1451/1452 на внешние ключи; sqlite-карта — 1555, 2067 и 787. Метод Translate сверяет код и оборачивает: postgres-версия делает fmt.Errorf("%w: %w", translatedErr, pgErr) — двойное заворачивание, поэтому errors.Is находит и gorm-ошибку, и оригинал драйвера. После этого один if errors.Is(err, gorm.ErrDuplicatedKey) работает на трёх базах — то, что в ent-курсе вы называли UniqueConstraintError, здесь носит имя ErrDuplicatedKey, а при выключенном TranslateError не сработает никогда.

Прогноз: что вернёт errors.Is

Прочитайте код и выберите, что он выведет:

db, _ := gorm.Open(postgres.Open(dsn),
    &gorm.Config{TranslateError: true})

// В таблице уже есть строка со slug "hleb",
// на Slug висит уникальный индекс.
err := db.Create(&TodoItem{Slug: "hleb"}).Error

if errors.Is(err, gorm.ErrDuplicatedKey) {
    fmt.Println("дубль")
} else {
    fmt.Println("не распознано")
}
// Что напечатает программа?

LIMIT-синтаксис: где LIMIT вообще живёт#

Семнадцатый урок научил вас Limit и Offset как методам GORM; на диалектном уровне выясняется, что LIMIT — не везде такой одинаковый, каким кажется. В SELECT тройка урока совпадает: LIMIT n OFFSET m понимают PostgreSQL, MySQL и SQLite, так что пагинация — самая переносимая часть языка. Методы GORM собирают именно эту форму, и сюрпризов здесь нет. Дальше начинается интересное: LIMIT и ORDER BY в модифицирующих командах. MySQL разрешает их прямо в UPDATE и DELETE — и мы уже видели доказательство в исходниках: mysql.go объявляет UpdateClauses и DeleteClauses с "ORDER BY", "LIMIT" на хвосте, то есть диалектор честно разрешит db.Limit(10).Delete(&TodoItem{}), и это будет легальный MySQL-запрос «удалить не больше десяти». PostgreSQL в UPDATE и DELETE прямого LIMIT не имеет: чтобы ограничить затронутые строки, пишут подзапрос по ctid или по первичному ключу — либо оставляют задачу WHERE-условию. SQLite поддерживает UPDATE/DELETE с LIMIT только как опцию сборки — флаг SQLITE_ENABLE_UPDATE_DELETE_LIMIT при компиляции библиотеки; в стандартных сборках go-sqlite3 этой опции нет, и запрос упадёт с синтаксической ошибкой. Практический вывод двоякий. Первый: пакетная обработка через «LIMIT в DELETE» пишется только под MySQL — под две другие базы делайте это SELECT-ом по ключам и DELETE WHERE id IN. Второй: GORM пропустит .Limit() в запросе изменения на любом диалекторе — проверка синтаксиса происходит в базе, и ошибка приедет из неё, а не из библиотеки. Диалект-знание экономит отладку: вы знаете, чья это поддержка, ещё до запуска.

SQLite in-memory: лаборатория без сервера#

Из трёх диалекторов SQLite единственный не требует сервера, и именно поэтому он стал любимцем тестов: база живёт в памяти процесса, поднимается за миллисекунды, умирает вместе с ним, не оставляя мусора. DSN-правила приносит mattn/go-sqlite3: классический «:memory:» создаёт частную базу на каждое соединение, а строка file:test.db?cache=shared&mode=memory из README — общую именованную память. Ловушка, о которую бьются все: GORM по умолчанию держит пул соединений, и второе соединение к «:memory:» увидит пустую базу — отсюда классический вопрос «почему no such table, ведь таблица создавалась». Лечится двумя способами: DSN с cache=shared — тогда все соединения пула смотрят на одну память — или db.DB() и SetMaxOpenConns(1), что честнее для теста: одна память, один писатель, никаких сюрпризов блокировок. Вторая особенность — сборка: официальный sqlite-драйвер основан на C-библиотеке и требует CGO, о чём README говорит прямо, а рядом перечисляет чисто-Go альтернативы — glebarez/sqlite и другие — с фразой Usage is identical — simply swap the import path: импорт меняется, диалектный код нет. В cross-compilation без CGO официальный драйвер не соберётся вовсе, и альтернатива — не роскошь, а необходимость. В sqlc-курсе SQLite-для-тестов посвящён отдельный урок; здесь зафиксируем границу: in-memory — это про скорость и изоляцию лаборатории, а инструменты вокруг тестов — транзакционные тесты, фабрики данных — тема двадцать четвёртого урока, который начинается ровно с этого моста. Память — не уменьшенная база, а настоящая SQLite: все диалектные отличия этого урока в ней присутствуют целиком.

Канон: полигон на in-memory, код без диалекта

GO
// Лаборатория: SQLite в памяти, ошибки - в нормализованном виде.
func openLab() (*gorm.DB, error) {
    db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"),
        &gorm.Config{TranslateError: true})
    if err != nil {
        return nil, err // CGO-сборка обязательна: это C-библиотека
    }
    // пул соединений - одна память на всех; альтернатива - SetMaxOpenConns(1)
    return db, nil
}

// Прикладной код, который не знает, какая база под ним:
item := TodoItem{Title: "Купить хлеб", Slug: "hleb"}
if err := db.Create(&item).Error; err != nil {
    // RETURNING или LastInsertId - решил диалектор, item.ID заполнен в обоих мирах
    return err
}
var got TodoItem
db.Where("slug = ?", "hleb").First(&got) // плейсхолдер ? - у mysql и sqlite
// этот же код с postgres.Open на живом сервере работает без правок

Канон мультибазовости: диалект-знание локализовано в двух местах — выбор Dialector при Open и осознанные SQL-строки условий. Внутри прикладного кода ни одного упоминания базы нет: и RETURNING, и плейсхолдеры, и возврат ключа решены диалектором. TranslateError: true включает нормализацию ошибок на всех базах.

Сборка: in-memory лаборатория с нормализацией ошибок

Соберите функцию лаборатории на SQLite: общая память для пула, включённый перевод ошибок и честная ловля дубля. Пропуски ___ отмечают места для кода.

func openLab() (*gorm.DB, error) { db, err := gorm.Open(sqlite.Open("file::memory:?=shared"), &gorm.Config{: true}) // переводить ошибки в gorm-типы if err != nil { return nil, err } return db, nil } err := db.Create(&TodoItem{Slug: "hleb"}).Error if errors.Is(err, gorm.) { // дубль: MySQL ответил бы 1062, PostgreSQL - 23505, // SQLite - "UNIQUE constraint failed", код }

Путь INSERT-а через диалектор

Соберите жизненный цикл вставки: как один db.Create доезжает до трёх разных SQL-миров.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Что унести в двадцать четвёртый урок#

Соберём правило портативности, которое урок прошёл по всем слоям. Уровень первый — код: db.Create, db.Where, ассоциации, транзакции одинаковы на трёх базах, потому что диалект-знание спрятано в Dialector — вы платите одним аргументом Open. Уровень второй — типы колонок: DataTypeOf переводит одну модель в три таблицы, и varchar(256) против text против numeric — это не ошибки, а переводы; проверяйте их SHOW CREATE TABLE-ом и задавайте size/type явно, когда точность важна. Уровень третий — SQL в строках условий: регистрозависимый ILIKE-поиск, jsonb-оператор @>, json_extract, бэктики — вы выехали за границу GORM, и переносить такое придётся руками; держите эти строки в скопах и репозиториях запросов, чтобы их было видно. Уровень четвёртый — ошибки: TranslateError превращает три карты кодов в один errors.Is, и это дешёвая страховка при переезде. Куда двигаться дальше: двадцать четвёртый урок — тестирование с GORM, и он начинается с того, что этот урок оставил в наследство — SQLite in-memory как полигон, на котором все диалектные правила всё ещё честные. Возьмите канон этого урока и прогоните свой домашний проект на всех трёх базах: список упавших мест — это и есть ваш личный портрет диалект-знания, и лучше узнать его до продакшена, чем после.

Карточки: мультибазовость и диалекты

Термин

Три официальных драйвера

нажмите, чтобы перевернуть
Определение

gorm.io/driver/postgres — поверх pgx, gorm.io/driver/mysql — поверх go-sql-driver/mysql, gorm.io/driver/sqlite — поверх mattn/go-sqlite3 (requires CGO). Чисто-Go альтернатива — glebarez/sqlite: «Usage is identical — simply swap the import path». Каждый пакет экспортирует Open(dsn) для gorm.Open.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Мультибазовость в GORM устроена через Dialector: ядро собирает запрос из clause-ов, а диалектор решает всё диалектное — плейсхолдеры (BindVarTo: $n против ?), цитаты (QuoteTo: двойные кавычки против бэктиков), типы колонок (DataTypeOf) и подмены clause-ов (ClauseBuilders).
  • Три официальных драйвера: gorm.io/driver/postgres поверх pgx, gorm.io/driver/mysql поверх go-sql-driver/mysql, gorm.io/driver/sqlite поверх mattn/go-sqlite3 с требованием CGO; pure-Go альтернативы (glebarez/sqlite) меняют только импорт.
  • RETURNING: Postgres дописывает его ко всем clause-спискам, SQLite держит сразу, MySQL — только на MariaDB ≥ 10.5 после SELECT VERSION(); классический MySQL читает LastInsertId, и default-выражения после Create остаются нулями.
  • Один serializer — три колонки: text (Postgres при Size 0), varchar(256) с DefaultStringSize (MySQL, под индексом 191 из-за utf8mb4), text (SQLite); boolean на SQLite — numeric, автоинкремент — bigserial, int auto_increment, integer PRIMARY KEY AUTOINCREMENT.
  • jsonb+GIN+@> — Postgres-only знание: MySQL отвечает типом json и функциональными индексами, SQLite — TEXT и json_extract; операторы диалектов в строках условий GORM не переводит — переносить руками.
  • clause.OnConflict один для всех баз: MySQL через ClauseBuilders пишет ON DUPLICATE KEY UPDATE, эмулирует DoNothing присваиванием `id`=`id` и игнорирует Columns; PostgreSQL требует цель конфликта.
  • Ошибки дублей: 23505 (Postgres), 1062 (MySQL), 1555/2067 (SQLite); с TranslateError: true каждый error_translator.go мапит коды в gorm.ErrDuplicatedKey с %w-обёрткой — один errors.Is на три базы, без настройки — три разных switch-а.
  • LIMIT/OFFSET в SELECT одинаков во всех трёх; ORDER BY и LIMIT в UPDATE/DELETE — MySQL-умение (видно по UpdateClauses mysql.go), Postgres требует подзапрос, SQLite — флаг сборки SQLITE_ENABLE_UPDATE_DELETE_LIMIT.
  • SQLite in-memory — лаборатория без сервера: «:memory:» приватна на соединение (ловушка «no such table» в пуле), лечится cache=shared или SetMaxOpenConns(1); инструменты тестов вокруг неё — тема следующего урока.

Домашнее задание

Соберите лабораторию мультибазовости поверх канона с todo_items. Шаг первый: поднимите PostgreSQL и MySQL локально, напишите переключатель конфигурации с тремя DSN из урока и убедитесь, что прикладной код — Create, Where, First — работает без правок на всех трёх базах; SQLite подключите через file::memory:?cache=shared и прогоните тот же код. Шаг второй: создайте модель с полем Tags []string и serializer:json, выполните миграции на трёх базах и сравните фактические типы колонок — d в psql, SHOW CREATE TABLE в MySQL, PRAGMA table_info в SQLite; выпишите три результата и объясните каждый через DataTypeOf. Шаг третий: на PostgreSQL добавьте jsonb-колонку с GIN-индексом и прогоните фильтр @> с EXPLAIN ANALYZE; на MySQL повторите функциональным индексом по выражению пути; на SQLite — json_extract-ом; сравните планы и запишите, где поиск мгновенный, а где полный проход. Шаг четвёртый: вставьте дубль Slug на трёх базах с TranslateError: true и без — проверьте, что errors.Is(err, gorm.ErrDuplicatedKey) срабатывает только с настройкой; затем временно отключите TranslateError и напишите три классификации руками: *mysql.MySQLError с кодом 1062, *pgconn.PgError с кодом 23505 и текст SQLite. Шаг пятый: попробуйте db.Limit(10).Delete(&TodoItem{}) на MySQL и на PostgreSQL — один запрос пройдёт, второй упадёт; поймите по исходникам mysql.go, почему. Финал: в тесте на «:memory:» снимите cache=shared и воспроизведите «no such table» на втором соединении пула, затем почините — и зафиксируйте в заметках проекта, какие диалект-строки в вашем коде потребуют внимания при переезде.

Личные заметкиПусто
Хранятся только в вашем браузере (GORM · Мультибазовость: диалекты — один код для Postgres, MySQL и SQLite в GORM)

Вопросы и ответы по уроку

Насколько безопасно переезжать между PostgreSQL, MySQL и SQLite с одним и тем же кодом GORM?

Ядро кода — Create, Find, Where, ассоциации, транзакции — переносится заменой одного аргумента gorm.Open, потому что всё диалектное спрятано в Dialector. Разрушаются три вещи: SQL в строках условий (диалектные операторы вроде @> и json_extract — GORM их не переводит), типы колонок (DataTypeOf даёт text против varchar(256) против numeric — сверяйте фактические схемы) и возвращение сгенерированных значений (default-выражения на MySQL после Create остаются нулями). Если вы использовали диалект-операторы или type:jsonb, при переезде каждый такой случай требует ручной замены.

Почему GORM.ErrDuplicatedKey не ловится, хотя дубль точно есть?

Потому что нормализация ошибок выключена по умолчанию. Она включается конфигом gorm.Config{TranslateError: true} при gorm.Open и работает через метод Translate диалектора: файл error_translator.go каждого драйвера держит карту кодов — 23505 у Postgres, 1062 у MySQL, 1555 и 2067 у SQLite — и заворачивает ошибку драйвера в gorm.ErrDuplicatedKey через %w. Без настройки приедет сырая ошибка: *mysql.MySQLError, *pgconn.PgError или текст SQLite, и один errors.Is на три базы не собрать.

Чем ':memory:' отличается от 'file::memory:?cache=shared'?

«:memory:» создаёт отдельную частную базу на каждое соединение: у GORM с пулом соединений второе соединение увидит пустую базу, и тест упадёт с «no such table». Строка file::memory:?cache=shared из README драйвера именует общую память: все соединения пула работают с одной и той же базой. Альтернатива — ограничить пул одним соединением через SetMaxOpenConns(1). И помните: официальный драйвер требует CGO, для сборок без CGO есть glebarez/sqlite с тем же API.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

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

Следующая ступень учебного пути

sqlc25 уроков~12 чпрактика25 уроков уже доступны

sqlc: типобезопасный код из SQL-запросов

Курс sqlc: настройка, запросы, параметры, NULL-типы, транзакции, джойны, пагинация, CI и сравнение с ORM. 25 уроков с практикой.

  • Настраивать sqlc.yaml для Postgres/MySQL/SQLite
  • Писать аннотированные запросы с типизированными параметрами
  • Работать с NULL, массивами и JSON-колонками
  • Организовывать транзакции через WithTx