Содержание урока · 10 разделов
- Соглашения об именовании: users, full_name, todo_items
- gorm.Model: что встраивает по умолчанию
- Теги полей: колонки, индексы, ограничения
- Частые теги GORM
- TableName и NamingStrategy: своя логика именования
- Типы полей: от string до time.Time
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Открываете базу после первых экспериментов, а там таблица todo_items и колонка full_name — откуда? Это не GORM капризничает, это соглашения сработали. Модель в GORM — самая обычная структура Go: никакого базового класса, никакой кодогенерации, никаких DSL. GORM один раз читает поля через рефлексию при первом обращении к модели, кэширует распарсенную схему и дальше работает с ней без затрат. Ключевая идея библиотеки — соглашения вместо конфигурации: если структура называется User, таблица будет users; если поле FullName, колонка будет full_name. Правила простые, но их надо знать наизусть, потому что по ним живёт каждый запрос. А когда соглашение мешает — есть два уровня ручной настройки: теги на полях и стратегия именования.
Соглашения об именовании: users, full_name, todo_items#
Три правила. Первое: имя таблицы — snake_case во множественном числе от имени структуры. User превращается в users, TodoItem — в todo_items, Address — в addresses (склонение делает библиотека inflection, и она знает про нерегулярные формы вроде person → people). Второе: имя колонки — snake_case от имени поля. FullName становится full_name, CreatedAt — created_at. Третье: Go-аббревиатуры GORM распознаёт и не разрывает — UserID превратится в user_id, а не в u_s_e_r_i_d, APIKey — в api_key, HTTPStatus — в http_status. Список «понятных» аббревиатур зашит в код: ID, API, URL, HTTP, JSON, SQL, UUID и другие.
Эти же правила работают на автоматические таблицы связей many-to-many — их мы будем разбирать в уроке 13, и там знание конвенций прямо спасает от сюрпризов. Пока достаточно запомнить: всё, что Go пишет CamelCase, база видит в snake_case, а единственное число становится множественным на уровне таблицы.
Структура и то, что получит база
GOtype TodoItem struct {
ID uint
Title string
Done bool
DueDate time.Time
}
// Таблица: "todo_items"
// Колонки: id, title, done, due_dateНи один тег не написан — GORM выведет всё из имён. ID получил роль первичного ключа автоматически: поле с именем ID и типом uint распознаётся как автоинкрементный ключ без явного primaryKey. Комментарий внизу — то, что создаст AutoMigrate, о котором подробнее в уроке 04.
gorm.Model: что встраивает по умолчанию#
Почти каждая модель в реальных проектах начинается со встраивания gorm.Model — стандартного набора служебных полей. Вот его настоящее определение из исходников:
Код
GOtype Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
type Post struct {
gorm.Model // встраивание — поля «врастают» в Post
Title string
Body string
}
// Таблица "posts" получит колонки:
// id, created_at, updated_at, deleted_at, title, bodyЧетыре поля, четыре обязанности. ID — автоинкрементный первичный ключ. CreatedAt GORM заполняет текущим временем при вставке, UpdatedAt — при каждом обновлении: руками трогать их не нужно. DeletedAt — самый хитрый: тип gorm.DeletedAt включает мягкое удаление — после Delete строка не удаляется, а лишь помечается временем, и обычные SELECT-запросы перестают её видеть. Подробно разберём в уроке 09; здесь важно, что колонка снабжена индексом, потому что deleted_at попадает почти в каждый запрос.
Теги полей: колонки, индексы, ограничения#
Когда имя поля недостаточно или нужна особая логика, в игру входят структурные теги. Тег gorm — строка с настройками через точку с запятой, каждая вида ключ или ключ:значение. Теги читаются при разборе схемы — то есть один раз, при первом использовании модели. Самые ходовые: column переименовать колонку, size задать размер текстового типа, uniqueIndex повесить уникальный индекс, index — обычный, type вписать SQL-тип вручную, default указать значение по умолчанию, not null запретить пустоты, а дефис — полностью выкинуть поле из таблицы (удобно для кэша и вычисляемых вещей).
Частые теги GORM#
| Тег | Действие | Пример |
|---|---|---|
| primaryKey | поле становится первичным ключом | gorm:"primaryKey" |
| column | переименовать колонку | gorm:"column:description" |
| size | размер строкового типа | gorm:"size:64" |
| type | явный SQL-тип | gorm:"type:decimal(10,2)" |
| uniqueIndex | уникальный индекс | gorm:"uniqueIndex" |
| index | обычный индекс | gorm:"index" |
| default | значение по умолчанию | gorm:"default:0" |
| not null | запрет NULL | gorm:"not null" |
| - | исключить поле из таблицы | gorm:"-" |
Модель каталога с тегами
GOtype Product struct {
gorm.Model
SKU string `gorm:"size:64;uniqueIndex"`
Title string `gorm:"not null"`
Price float64 `gorm:"type:decimal(10,2)"`
Note string `gorm:"column:description;default:''"`
Cache string `gorm:"-"`
}
// В таблице "products" появятся:
// id, created_at, updated_at, deleted_at,
// sku (varchar(64), unique), title (not null),
// price (decimal(10,2)), description (default '')SKU короткий и обязанный быть уникальным — size:64 плюс uniqueIndex. Price — деньги, значит точный тип decimal, а не double, который накапливает ошибку округления. Note переименована в description тегом column и получает дефолт. Cache вообще не попадёт в базу — это поле времени исполнения.
TableName и NamingStrategy: своя логика именования#
Соглашения — это дефолт, а не закон. Метод TableName возвращает имя таблицы напрямую и имеет высший приоритет: хватит, когда работаете с legacy-схемой, где таблицы называются по-другому, или когда компания держит префиксы. Если правило глобальное (например, все таблицы с префиксом shop_), вместо десятка методов TableName настраивается NamingStrategy в конфиге подключения: TablePrefix добавляет префикс ко всем таблицам, SingularTable запрещает множественное число — структура User будет мапиться на user, а не users. Есть и NameReplacer для точечных замен имён. Стратегию передают в gorm.Open через поле NamingStrategy конфига, тип берётся из пакета gorm.io/gorm/schema.
Переопределяем имена
GO// Точечно: для одной модели
func (Product) TableName() string {
return "catalog_products"
}
// Глобально: для всех моделей
import "gorm.io/gorm/schema"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "shop_",
SingularTable: true,
},
})
// Product теперь живёт в "shop_product"Метод TableName объявляют на значении-приёмнике, не на указателе — GORM вызывает его и на значениях. После включения TablePrefix все таблицы получат префикс shop_, а SingularTable оставит имена в единственном числе. Миграции и запросы подхватят новые имена автоматически.
Написали uniquIndex вместо uniqueIndex? Никто не скажет ни слова: незнакомые ключи GORM молча пропускает, схема соберётся без уникального индекса, а первый дубликат потом уйдёт в базу. Я не доверяю тегам на глаз: после первой миграции смотрю фактическую схему или проверяю в тесте через db.Migrator().HasIndex — об этом в уроке 04.
Типы полей: от string до time.Time#
Тип колонки GORM выводит из типа поля, а точный SQL-тип зависит от диалекта. Строка без size превращается в text (Postgres) или varchar нужного размера; int — в bigint, bool — в boolean, float64 — в double precision, time.Time — в timestamptz, []byte — в binary/blob. Правило денег повторю отдельно: для сумм используйте явный тип:decimal или целые копейки в int — float64 в базе — источник «копеечных» расхождений в бухгалтерии.
Отдельная история — отсутствующие значения. Обычный string не различает пустую строку и NULL: оба превратятся в "". Если колонка допускает NULL, поле делают указателем (string, time.Time) — GORM запишет NULL, когда указатель nil. Альтернатива — типы sql.NullString и compañía из стандартной библиотеки; сравнение обоих подходов и третья опция, sql.Null[T] из Go 1.22, ждут вас в уроке 21. А если хочется хранить в колонке структуру или карту — это тег serializer:json и пользовательские типы, урок 22.
Проверка: соглашения об именовании
Как GORM назовёт таблицу для структуры TodoItem?
Проверка: молчаливые теги
Поле помечено gorm:"uniquIndex" (опечатка). Что произойдёт при миграции?
Какими будут имя таблицы и колонка?
Прочитайте код и выберите, что он выведет:
type Order struct {
gorm.Model
OrderNo string `gorm:"size:32;uniqueIndex"`
Total float64
}
// Как назовутся таблица и колонка OrderNo?Заполните пропуски: модель статьи
Восстановите теги и метод переопределения имени таблицы: у Slug — размер 64 и уникальный индекс, у Views — дефолт 0, таблица — blog_articles.
Карточки: словарь моделей
Что дальше по курсу#
Модель описана — пора её материализовать. В четвёртом уроке — автомиграции: что делает AutoMigrate, чего он не умеет и когда нужны версионированные миграции. В пятом и шестом — создание записей и чтение через First и Find. Теги индексов мы использовали мельком — в уроке 18 разберём индексы всерьёз: составные, частичные и как проверить план запроса. А если хочется видеть, как модели живут в настоящем проекте, — финальный урок курса собирает слой данных блога целиком.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Модель — обычная структура Go: GORM разбирает её рефлексией один раз и кэширует схему.
- Таблица — множественное число в snake_case, колонка — snake_case поля; UserID даёт user_id.
- gorm.Model добавляет ID, CreatedAt, UpdatedAt и DeletedAt с индексом (мягкое удаление).
- Теги уточняют схему: column, size, type, uniqueIndex, index, default, not null, дефис для исключения поля.
- TableName() переопределяет имя таблицы точечно, NamingStrategy — глобально (префиксы, единственное число).
- Опечатка в теге не вызывает ошибку — проверяйте фактическую схему после миграции.
- Для денег — decimal или целые копейки; NULL моделируется указателями или sql.Null* (урок 21).
Домашнее задание
Объявите модель Invoice: gorm.Model, поле Number с size:32 и уникальным индексом, Amount с типом decimal(12,2), поле internalMemo с тегом дефис. Локально подключите SQLite, выполните AutoMigrate и откройте файл базы (sqlite3 app.db, затем .schema invoices): убедитесь, что колонки называются так, как вы ожидали, уникальный индекс существует, а internalMemo в таблице нет.
Личные заметкиПусто
Вопросы и ответы по уроку
Обязательно ли встраивать gorm.Model?
Нет: модель может быть любой структурой без gorm.Model — тогда не будет автотаймстампов и мягкого удаления, а первичным ключом станет поле с именем ID или тег primaryKey. Для таблиц-логов и join-таблиц чистые структуры — норм.
Как назвать колонку, если поле называется APIKey?
api_key: GORM знает список Go-аббревиатур (ID, API, URL, HTTP, JSON и другие) и не рвёт их на буквы. Полный список — в исходниках пакета gorm.io/gorm/schema.
Что делать, если таблица уже существует со «странными» именами?
Переопределите имена точечно: метод TableName() для таблицы и тег column для колонок. Либо настройте NamingStrategy, если закономерность одна на весь проект — например, префикс legacy_.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
sqlc: типобезопасный код из SQL-запросов
Курс sqlc: настройка, запросы, параметры, NULL-типы, транзакции, джойны, пагинация, CI и сравнение с ORM. 25 уроков с практикой.
- Настраивать sqlc.yaml для Postgres/MySQL/SQLite
- Писать аннотированные запросы с типизированными параметрами
- Работать с NULL, массивами и JSON-колонками
- Организовывать транзакции через WithTx