Основи GORM¶
GORM зіставляє структури з таблицями й генерує SQL. Він прибирає більшу
частину шаблонного коду Scan з
database/sql, і привносить
поведінку, про яку варто знати — одна з них мовчки записує неправильне
значення у вашу базу даних, нічого про це не повідомляючи.
Модулі:
gorm.io/gormтаgorm.io/driver/postgres.
Відкриття¶
db, err := gorm.Open(postgres.New(postgres.Config{
DSN: dsn,
}), &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn),
SkipDefaultTransaction: true,
})
*gorm.DB обгортає пул database/sql, тож налаштування пулу
відбувається саме там:
Дві опції конфігурації варті свідомого налаштування. SkipDefaultTransaction
вимикає неявну транзакцію, якою GORM обгортає кожен окремий запис —
відчутна економія, коли ви керуєте транзакціями самостійно. І
встановлюйте рівень Logger явно, бо стандартний логує кожен повільний
запит у stdout у форматі, який ніхто не парсить.
Моделі¶
type Base struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
type Book struct {
Base
Title string `gorm:"not null"`
AuthorID uuid.UUID `gorm:"type:uuid;index"`
Draft bool `gorm:"not null;default:true"`
Notes *string
Ignored string `gorm:"-"`
}
Вбудовування Base дає кожній таблиці її id і часові мітки — вбудовування
структур зі статті про структури,
з тегами, що просуваються разом із полями.
| Тег | Ефект |
|---|---|
primaryKey |
первинний ключ |
type:uuid |
тип колонки SQL |
not null, unique, index |
обмеження |
default:expr |
значення колонки за замовчуванням — див. пастку нижче |
column:name |
перевизначити виведену назву колонки |
autoCreateTime / autoUpdateTime |
підтримується GORM |
- |
не зберігається |
Імена виводяться за домовленістю: Book → books, AuthorID →
author_id. Одруківка в тегу column: компілюється без проблем і
мовчки зіставляється з неправильною колонкою — саме тому варта наявності
перевірка на розбіжність зі схемою.
*string — колонка, що допускає NULL; звичайний string — NOT NULL
з '' як нульовим значенням. Ця відмінність зі статті про
кодування JSON
застосовується й тут.
Масиви-колонки Postgres потребують lib/pq¶
GORM, що працює на драйвері pgx, усе одно не зіставляє Postgres
text[] з []string самостійно. Тип, що це робить — pq.StringArray
з github.com/lib/pq:
type Tag struct {
ID int64 `gorm:"primaryKey"`
Name string `gorm:"uniqueIndex"`
Langs pq.StringArray `gorm:"type:text[];not null;default:'{}'"`
}
t := Tag{Name: "go", Langs: pq.StringArray{"go", "python"}}
db.Create(&t)
var back Tag
db.First(&back, "name = ?", "go")
fmt.Println(back.Langs, len(back.Langs)) // output: [go python] 2
Це дивує людей, бо pgx зіставляє масиви з
[]string нативно — але це власний API pgx. Пройдіть через GORM, і
ви повернетесь до типу driver.Valuer/sql.Scanner, яким і є
pq.StringArray. Є pq.Int64Array та подібні для інших типів
елементів, і pq.Array(&v) обгортає зріз для одноразового запиту.
Тож кодова база може залежати від lib/pq суто заради цих типів,
використовуючи pgx як фактичний драйвер. Це не помилка; це звичайне
влаштування.
Пастка нульового значення з default:¶
Це те, що варто засвоїти назавжди. GORM пропускає поля з нульовим
значенням у згенерованому INSERT, тож натомість застосовується
значення за замовчуванням бази даних, а не те, яке ви встановили:
b := Book{Title: "Explicitly not a draft", Draft: false}
db.Create(&b)
var back Book
db.First(&back, "id = ?", b.ID)
fmt.Println(back.Draft) // output: true
Ви написали false. У базі даних лежить true. Жодної помилки ніде.
Часто повторюване виправлення — Select. Воно не працює — усі три
форми все одно дають true:
Насправді виправляють це дві речі. Поле-вказівник, де nil і
&false розрізняються:
type Book struct {
Draft *bool `gorm:"not null;default:true"`
}
f := false
db.Create(&Book{Title: "x", Draft: &f}) // stored: false
Або створення з мапи, в якій немає нульових значень, щоб їх пропускати:
Та сама пастка застосовується до Updates зі структурою:
Updates з мапою оновлює точно те, що ви перелічили:
Найпростіший захист — взагалі уникати default: на булевих і числових
полях і встановлювати значення в коді Go. Якщо вам потрібне значення за
замовчуванням бази даних, зробіть поле вказівником.
Читання¶
Передавайте контекст через WithContext у кожному виклику. Без нього
запит не має скасування, так само як і з простим драйвером.
First повертає сентинел, коли нічого не знайдено:
err := db.First(&book, "title = ?", "nope").Error
fmt.Println(errors.Is(err, gorm.ErrRecordNotFound)) // output: true
Find — ні:
var books []Book
r := db.Where("title = ?", "nope").Find(&books)
fmt.Println(r.Error, r.RowsAffected, len(books))
// output: <nil> 0 0
Порожній результат — не помилка для запиту списку. Перевіряйте
RowsAffected або len, а не Error.
Кожен виклик повертає *gorm.DB, що несе Error і RowsAffected.
Перевірка .Error — це еквівалент if err != nil, і забути про це —
найлегша помилка тут — нічого в системі типів цього не вимагає.
Запис¶
Create заповнює первинний ключ і часові мітки назад у вашу структуру.
Порушення обмежень виринають як помилка драйвера, тож
errors.As на *pgconn.PgError все ще
працює:
Хуки¶
Методи з зарезервованими іменами запускаються навколо операцій:
func (b *Book) BeforeCreate(tx *gorm.DB) error {
if b.Title == "" {
return errors.New("title is required")
}
return nil
}
Повернення помилки скасовує запис. Також доступні: AfterCreate,
BeforeUpdate, BeforeDelete та інші.
Використовуйте їх ощадливо. Хук — це поведінка, що спрацьовує непомітно з місця виклику, що ускладнює трасування — валідація зазвичай зрозуміліша в сервісному шарі.
AutoMigrate — для розробки¶
Він створює таблиці й додає відсутні колонки. Він не видалятиме колонки, безпечно змінюватиме типи чи давати diff, придатний для рецензування, і не має поняття про одноразовий запуск. Використовуйте його в тестах і локальній розробці; використовуйте goose для всього, що ви деплоїте.
Асоціації¶
type Author struct {
Base
Name string `gorm:"not null;unique"`
Books []Book `gorm:"foreignKey:AuthorID"`
}
Без Preload a.Books порожній — GORM не завантажує ліниво. Будьте
свідомі: попереднє завантаження асоціацій ендпоінта списку — це те, як
ви отримуєте патерн N+1 запитів, і Joins часто краща відповідь.
З досвіду Python: GORM приблизно як ORM SQLAlchemy з тегами в стилі Django замість декларативної схеми. Пастка нульового значення не має аналогу в Python, бо там
NoneіFalse— різні речі — у Go вони той самийfalse, якщо ви не використовуєте вказівник.
Швидка довідка¶
| Задача | Форма |
|---|---|
| відкрити | gorm.Open(postgres.New(...), &gorm.Config{}) |
| налаштування пулу | db.DB(), потім сетери database/sql |
| контекст | db.WithContext(ctx) у кожному виклику |
| перевірити провал | .Error на поверненому *gorm.DB |
| не знайдено | errors.Is(err, gorm.ErrRecordNotFound) — лише First |
| порожній список | Find не повертає помилку; перевірте RowsAffected |
нульове значення + default: |
використовуйте *bool, або Create/Updates з мапою |
| асоціації | Preload("Books") — ніколи не ліниво |
| схема | AutoMigrate у розробці, справжні міграції в продакшені |