Skip to content

ORM-библиотека Gorm

Официальная документация: GORM - The fantastic ORM library for Golang, aims to be developer friendly.

Репозиторий: go-gorm/gorm: The fantastic ORM library for Golang, aims to be developer friendly (github.com)

В сообществе Go для взаимодействия с базой данных существуют два лагеря. Один лагерь предпочитает более простые библиотеки, такие как sqlx, которые не так мощны, но позволяют полностью контролировать SQL и оптимизировать производительность до предела. Другой лагерь предпочитает ORM для эффективности разработки, что может сэкономить много ненужных проблем во время разработки. Когда речь заходит об ORM, в сообществе Go абсолютно невозможно обойти gorm. Это очень хорошо зарекомендовавшая себя ORM, похожая на относительно молодые, такие как xorm, ent и т.д. Эта статья о gorm. Эта статья объясняет только её базовое вводное содержание, просто как отправная точка. Чтобы узнать больше деталей, вы можете прочитать официальную документацию. Её китайская документация довольно полная, и автор также является одним из переводчиков документации gorm.

Особенности

  • Полнофункциональная ORM
  • Ассоциации (Has One, Has Many, Belongs To, Many To Many, Полиморфизм, Наследование одной таблицы)
  • Хуки (Before/After Create/Save/Update/Delete/Find)
  • Предзагрузка с жадной загрузкой (Preload, Joins)
  • Транзакции, вложенные транзакции, точки сохранения, откат к сохранённым точкам
  • Контекст, режим подготовленных выражений, режим DryRun
  • Пакетная вставка, FindInBatches, Find/Create с Map, CRUD с SQL-выражениями, Context Valuer
  • SQL Builder, Upsert, блокировка, подсказки Optimizer/Index/Comment, именованные параметры, подзапросы
  • Составной первичный ключ, индекс, ограничение
  • Автоматическая миграция
  • Пользовательский логгер
  • Гибкий расширяемый API плагинов: Database Resolver (несколько баз данных, разделение чтения/записи), Prometheus...
  • Каждая функция тщательно протестирована
  • Удобство для разработчика

У Gorm есть некоторые недостатки. Например, почти все параметры методов имеют пустой тип интерфейса. Без просмотра документации вы, вероятно, не узнаете, что передавать. Иногда можно передать struct, иногда строку, иногда map, иногда slice. Семантика несколько неопределённа, и во многих случаях всё ещё нужно писать SQL вручную.

В качестве альтернатив есть две ORM, которые можно попробовать. Первая — aorm, которая была открыта не так давно. Она больше не требует писать имена полей таблиц вручную, в основном использует цепочечные операции и основана на рефлексии. Поскольку количество звёзд невелико, можно подождать и посмотреть. Вторая — ent, открытая facebook. Она также поддерживает цепочечные операции, и в большинстве случаев не нужно писать SQL вручную. Её философия дизайна основана на графах (из структур данных), а реализация основана на генерации кода, а не на рефлексии (с чем я больше согласен). Однако документация только на английском, что имеет определённый порог обучения.

Установка

Установите библиотеку gorm

sh
$ go get -u gorm.io/gorm

Подключение

Gorm в настоящее время поддерживает следующие базы данных

  • MySQL: "gorm.io/driver/mysql"
  • PostgreSQL: "gorm.io/driver/postgres"
  • SQLite: "gorm.io/driver/sqlite"
  • SQL Server: "gorm.io/driver/sqlserver"
  • TIDB: "gorm.io/driver/mysql", TIDB совместим с протоколом mysql
  • ClickHouse: "gorm.io/driver/clickhouse"

Кроме того, есть некоторые другие драйверы баз данных, предоставленные сторонними разработчиками, такие как драйвер oracle CengSin/oracle. В этой статье для демонстрации будет использоваться MySQL. Какую бы базу данных вы ни использовали, нужно установить соответствующий драйвер. Здесь мы устанавливаем драйвер gorm для MySQL.

sh
$ go get -u gorm.io/driver/mysql

Затем подключаемся к базе данных, используя dsn (имя источника данных). Библиотека драйвера автоматически разберёт dsn в соответствующую конфигурацию.

go
package main

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
  "log/slog"
)

func main() {
  dsn := "root:123456@tcp(192.168.48.138:3306)/hello?charset=utf8mb4&parseTime=True&loc=Local"
  db, err := gorm.Open(mysql.Open(dsn))
  if err != nil {
    slog.Error("ошибка подключения к БД", err)
  }
  slog.Info("подключение к БД успешно")
}

Или вручную передать конфигурацию

go
package main

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
  "log/slog"
)

func main() {
  db, err := gorm.Open(mysql.New(mysql.Config{}))
  if err != nil {
    slog.Error("ошибка подключения к БД", err)
  }
  slog.Info("подключение к БД успешно")
}

Оба метода эквивалентны; это зависит от вашего предпочтения использования.

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

Передавая структуру конфигурации gorm.Config, мы можем контролировать некоторые поведения gorm.

go
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

Ниже приведены некоторые простые объяснения. Вы можете настроить в соответствии с вашими потребностями.

go
type Config struct {
  // Отключить транзакцию по умолчанию, gorm запустит транзакцию для одиночного create и update для поддержания согласованности данных
  SkipDefaultTransaction bool
  // Пользовательская стратегия именования
  NamingStrategy schema.Namer
  // Сохранить полные ассоциации
  FullSaveAssociations bool
  // Пользовательский логгер
  Logger logger.Interface
  // Пользовательская nowfunc, используется для внедрения полей CreatedAt и UpdatedAt
  NowFunc func() time.Time
  // Генерировать только SQL без выполнения
  DryRun bool
  // Использовать подготовленные выражения
  PrepareStmt bool
  // После установления подключения, ping базы данных
  DisableAutomaticPing bool
  // Игнорировать внешние ключи при миграции базы данных
  DisableForeignKeyConstraintWhenMigrating bool
  // Игнорировать ссылки на ассоциации при миграции базы данных
  IgnoreRelationshipsWhenMigrating bool
  // Отключить вложенные транзакции
  DisableNestedTransaction bool
  // Разрешить глобальное обновление, обновление без where clause
  AllowGlobalUpdate bool
  // Запросить все поля таблицы
  QueryFields bool
  // Размер пакетного создания
  CreateBatchSize int
  // Включить перевод ошибок
  TranslateError bool

  // ClauseBuilders построитель clause
  ClauseBuilders map[string]clause.ClauseBuilder
  // ConnPool пул подключений к БД
  ConnPool ConnPool
  // Dialector диалектор базы данных
  Dialector
  // Plugins зарегистрированные плагины
  Plugins map[string]Plugin

  callbacks  *callbacks
  cacheStore *sync.Map
}

Модель

В gorm модель соответствует таблице базы данных. Обычно она представлена struct, например, следующий struct.

go
type Person struct {
  Id      uint
  Name    string
  Address string
  Mom     string
  Dad     string
}

Внутренние поля struct могут состоять из базовых типов данных и типов, реализующих интерфейсы sql.Scanner и sql.Valuer. По умолчанию имя таблицы, сопоставленной struct Person, — persons, это стиль snake_case во множественном числе, разделённый подчёркиваниями. Имена столбцов также в стиле snake_case, например, Id соответствует имени столбца id. Gorm также предоставляет некоторые способы настройки этого.

Указание имён столбцов

Через теги struct мы можем указать имена столбцов для полей struct. Таким образом, во время сопоставления сущностей gorm будет использовать указанные имена столбцов.

go
type Person struct {
  Id      uint   `gorm:"column:ID;"`
  Name    string `gorm:"column:Name;"`
  Address string
  Mom     string
  Dad     string
}

Указание имён таблиц

Реализуя интерфейс Table, вы можете указать имя таблицы. У него только один метод, который возвращает имя таблицы.

go
type Tabler interface {
  TableName() string
}

В реализованном методе возвращается строка person. Во время миграции базы данных gorm создаст таблицу с именем person.

go
type Person struct {
  Id      uint   `gorm:"column:ID;"`
  Name    string `gorm:"column:Name;"`
  Address string
  Mom     string
  Dad     string
}

func (p Person) TableName() string {
  return "person"
}

Для стратегий именования вы также можете передать свою реализацию стратегии при создании подключения для достижения пользовательских эффектов.

Отслеживание времени

go
type Person struct {
  Id      uint
  Name    string
  Address string
  Mom     string
  Dad     string

  CreatedAt sql.NullTime
  UpdatedAt sql.NullTime
}

func (p Person) TableName() string {
  return "person"
}

При наличии полей CreatedAt или UpdatedAt, во время создания или обновления записи, если их нулевое значение, gorm автоматически использует time.Now() для установки времени.

go
db.Create(&Person{
    Name:    "jack",
    Address: "usa",
    Mom:     "lili",
    Dad:     "tom",
  })

// INSERT INTO `person` (`name`,`address`,`mom`,`dad`,`created_at`,`updated_at`) VALUES ('jack','usa','lili','tom','2023-10-25 14:43:57.16','2023-10-25 14:43:57.16')

Gorm также поддерживает отслеживание временных меток

go
type Person struct {
  Id      uint   `gorm:"primaryKey;"`
  Name    string `gorm:"primaryKey;"`
  Address string
  Mom     string
  Dad     string

  // наносекунды
  CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
  // миллисекунды
  UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}

Тогда во время выполнения Create это эквивалентно следующему SQL

sql
INSERT INTO `person` (`name`,`address`,`mom`,`dad`,`created_at`,`updated_at`) VALUES ('jack','usa','lili','tom',1698216540519000000,1698216540)

В реальных ситуациях, если вам нужно отслеживание времени, я рекомендую хранить временные метки на бэкенде, что проще обрабатывать в сценариях с несколькими часовыми поясами.

Модель

Gorm предоставляет предустановленную структуру Model, которая содержит первичный ключ ID, два поля отслеживания времени и поле записи мягкого удаления.

go
type Model struct {
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt DeletedAt `gorm:"index"`
}

Для использования просто встройте её в вашу модель сущности.

go
type Order struct {
  gorm.Model
  Name string
}

Таким образом, она автоматически получит все функции gorm.Model.

Первичный ключ

По умолчанию поле с именем Id является первичным ключом. Вы можете использовать теги struct для указания поля первичного ключа.

go
type Person struct {
  Id      uint `gorm:"primaryKey;"`
  Name    string
  Address string
  Mom     string
  Dad     string

  CreatedAt sql.NullTime
  UpdatedAt sql.NullTime
}

Несколько полей образуют составной первичный ключ

go
type Person struct {
  Id      uint `gorm:"primaryKey;"`
  Name    string `gorm:"primaryKey;"`
  Address string
  Mom     string
  Dad     string

  CreatedAt sql.NullTime
  UpdatedAt sql.NullTime
}

Индекс

Вы можете указать индексы столбцов через тег struct index

go
type Person struct {
  Id      uint   `gorm:"primaryKey;"`
  Name    string `gorm:"primaryKey;"`
  Address string `gorm:"index:idx_addr,unique,sort:desc;"`
  Mom     string
  Dad     string

  // наносекунды
  CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
  // миллисекунды
  UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}

В приведённом struct для поля Address создаётся уникальный индекс. Два поля, использующие одно и то же имя индекса, создадут составной индекс

go
type Person struct {
    Id      uint   `gorm:"primaryKey;"`
    Name    string `gorm:"primaryKey;"`
    Address string `gorm:"index:idx_addr,unique;"`
    School  string `gorm:"index:idx_addr,unique;"`
    Mom     string
    Dad     string

    // наносекунды
    CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
    // миллисекунды
    UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}

Внешний ключ

В struct отношения внешних ключей определяются путём встраивания struct, например

go
type Person struct {
  Id   uint `gorm:"primaryKey;"`
  Name string

  MomId uint
  Mom   Mom `gorm:"foreignKey:MomId;"`

  DadId uint
  Dad   Dad `gorm:"foreignKey:DadId;"`
}

type Mom struct {
  Id   uint
  Name string

  Persons []Person `gorm:"foreignKey:MomId;"`
}

type Dad struct {
  Id   uint
  Name string

  Persons []Person `gorm:"foreignKey:DadId;"`
}

В примере struct Person имеет два внешних ключа, ссылающихся на первичные ключи struct Dad и Mom соответственно. По умолчанию он ссылается на первичный ключ. Person имеет отношение один-к-одному с Dad и Mom — у человека может быть только один отец и одна мать. Dad и Mom имеют отношение один-ко-многим с Person, потому что у отцов и матерей может быть несколько детей.

go
Mom   Mom `gorm:"foreignKey:MomId;"`

Цель встраивания struct — облегчить указание внешних ключей и ссылок. По умолчанию формат имени поля внешнего ключа — ReferencedTypeName+Id, например, MomId. По умолчанию он ссылается на первичный ключ. Вы можете указать поле для ссылки через теги struct

go
type Person struct {
  Id   uint `gorm:"primaryKey;"`
  Name string

  MomId uint
  Mom   Mom `gorm:"foreignKey:MomId;references:Sid;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`

  DadId uint
  Dad   Dad `gorm:"foreignKey:DadId;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

type Mom struct {
  Id   uint
  Sid  uint `gorm:"uniqueIndex;"`
  Name string

  Persons []Person `gorm:"foreignKey:MomId;"`
}

Среди них constraint:OnUpdate:CASCADE,OnDelete:SET NULL; определяет ограничение внешнего ключа.

Хуки

Модель сущности может настраивать хуки

  • Create
  • Update
  • Delete
  • Query

Соответствующие интерфейсы следующие

go
// Вызывается перед create
type BeforeCreateInterface interface {
    BeforeCreate(*gorm.DB) error
}

// Вызывается после create
type AfterCreateInterface interface {
    AfterCreate(*gorm.DB) error
}

// Вызывается перед update
type BeforeUpdateInterface interface {
    BeforeUpdate(*gorm.DB) error
}

// Вызывается после update
type AfterUpdateInterface interface {
    AfterUpdate(*gorm.DB) error
}

// Вызывается перед save
type BeforeSaveInterface interface {
    BeforeSave(*gorm.DB) error
}

// Вызывается после save
type AfterSaveInterface interface {
    AfterSave(*gorm.DB) error
}

// Вызывается перед delete
type BeforeDeleteInterface interface {
    BeforeDelete(*gorm.DB) error
}

// Вызывается после delete
type AfterDeleteInterface interface {
    AfterDelete(*gorm.DB) error
}

// Вызывается после query
type AfterFindInterface interface {
    AfterFind(*gorm.DB) error
}

Struct могут настраивать поведения, реализуя эти интерфейсы.

Теги

Ниже приведены некоторые теги, поддерживаемые gorm

Имя тегаОписание
columnУказывает имя столбца БД
typeТип данных столбца, рекомендуется использовать совместимые универсальные типы, например, все базы данных поддерживают bool, int, uint, float, string, time, bytes и могут использоваться с другими тегами, например, not null, size, autoIncrement... Также поддерживается указание типов данных БД, таких как varbinary(8). При использовании указанных типов данных БД это должен быть полный тип данных БД, например, MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
serializerУказывает сериализатор для сериализации или десериализации данных в/из БД, например, serializer:json/gob/unixtime
sizeОпределяет размер или длину типа данных столбца, например, size: 256
primaryKeyОпределяет столбец как первичный ключ
uniqueОпределяет столбец как уникальный ключ
defaultОпределяет значение по умолчанию столбца
precisionУказывает точность столбца
scaleУказывает масштаб столбца
not nullУказывает столбец как NOT NULL
autoIncrementУказывает столбец как автоинкремент
autoIncrementIncrementШаг автоинкремента, контролирует интервал между последовательными записями
embeddedВстроенное поле
embeddedPrefixПрефикс имени столбца для встроенного поля
autoCreateTimeОтслеживает текущее время при создании, для полей int отслеживает секунды временной метки, можно использовать nano/milli для отслеживания наносекунд/миллисекунд временной метки, например, autoCreateTime:nano
autoUpdateTimeОтслеживает текущее время при создании/обновлении, для полей int отслеживает секунды временной метки, можно использовать nano/milli для отслеживания наносекунд/миллисекунд временной метки, например, autoUpdateTime:milli
indexСоздаёт индекс на основе параметров, несколько полей, использующих одно и то же имя, создают составной индекс, подробности см. Indexes
uniqueIndexТо же, что и index, но создаёт уникальный индекс
checkСоздаёт ограничение проверки, например, check:age > 13, подробности см. Constraints
<-Устанавливает разрешение на запись поля, <-:create только создание, <-:update только обновление, <-:false нет разрешения на запись, <- разрешение на создание и обновление
->Устанавливает разрешение на чтение поля, ->:false нет разрешения на чтение
-Игнорирует это поле, - означает нет чтения/записи, -:migration означает нет разрешения на миграцию, -:all означает нет чтения/записи/миграции
commentДобавляет комментарий для поля во время миграции
foreignKeyУказывает столбец текущей модели как внешний ключ для таблицы соединения
referencesУказывает имя столбца ссылочной таблицы, который будет сопоставлен как внешний ключ таблицы соединения
polymorphicУказывает полиморфный тип, например, имя модели
polymorphicValueУказывает полиморфное значение, по умолчанию имя таблицы
many2manyУказывает имя таблицы соединения
joinForeignKeyУказывает имя столбца внешнего ключа в таблице соединения, который будет сопоставлен с текущей таблицей
joinReferencesУказывает имя столбца внешнего ключа в таблице соединения, который будет сопоставлен со ссылочной таблицей
constraintОграничения отношений, например, OnUpdate, OnDelete

Миграция

Метод AutoMigrate помогает нам с автоматической миграцией. Он создаст таблицы, ограничения, индексы, внешние ключи и т.д.

go
func (db *DB) AutoMigrate(dst ...interface{}) error

Например

go
type Person struct {
  Id      uint   `gorm:"primaryKey;"`
  Name    string `gorm:"type:varchar(100);uniqueIndex;"`
  Address string
}

type Order struct {
  Id   uint
  Name string
}

db.AutoMigrate(Person{}, Order{})
// CREATE TABLE `person` (`id` bigint unsigned AUTO_INCREMENT,`name` varchar(100),`address` longtext,PRIMARY KEY (`id`),UNIQUE INDEX `idx_person_name` (`name`))
// CREATE TABLE `orders` (`id` bigint unsigned AUTO_INCREMENT,`name` longtext,PRIMARY KEY (`id`))

Или мы можем оперировать вручную, получив доступ к интерфейсу Migrator через метод Migrator

go
func (db *DB) Migrator() Migrator

Он поддерживает следующие методы интерфейса

go
type Migrator interface {
  // AutoMigrate
  AutoMigrate(dst ...interface{}) error

  // Database
  CurrentDatabase() string
  FullDataTypeOf(*schema.Field) clause.Expr
  GetTypeAliases(databaseTypeName string) []string

  // Tables
  CreateTable(dst ...interface{}) error
  DropTable(dst ...interface{}) error
  HasTable(dst interface{}) bool
  RenameTable(oldName, newName interface{}) error
  GetTables() (tableList []string, err error)
  TableType(dst interface{}) (TableType, error)

  // Columns
  AddColumn(dst interface{}, field string) error
  DropColumn(dst interface{}, field string) error
  AlterColumn(dst interface{}, field string) error
  MigrateColumn(dst interface{}, field *schema.Field, columnType ColumnType) error
  HasColumn(dst interface{}, field string) bool
  RenameColumn(dst interface{}, oldName, field string) error
  ColumnTypes(dst interface{}) ([]ColumnType, error)

  // Views
  CreateView(name string, option ViewOption) error
  DropView(name string) error

  // Constraints
  CreateConstraint(dst interface{}, name string) error
  DropConstraint(dst interface{}, name string) error
  HasConstraint(dst interface{}, name string) bool

  // Indexes
  CreateIndex(dst interface{}, name string) error
  DropIndex(dst interface{}, name string) error
  HasIndex(dst interface{}, name string) bool
  RenameIndex(dst interface{}, oldName, newName string) error
  GetIndexes(dst interface{}) ([]Index, error)
}

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

Указание комментариев таблиц

Во время миграции, если вы хотите добавить комментарии к таблице, вы можете установить это следующим образом

go
db.Set("gorm:table_options", " comment 'person table'").Migrator().CreateTable(Person{})

Обратите внимание, что если вы используете метод AutoMigrate() для миграции и между struct есть отношения ссылок, gorm рекурсивно создаст сначала ссылочные таблицы. Это приведёт к тому, что и ссылочная таблица, и ссылающаяся таблица будут иметь дублирующиеся комментарии. Поэтому рекомендуется использовать метод CreateTable для создания.

TIP

При создании таблиц метод CreateTable требует, чтобы ссылочная таблица была создана перед ссылающейся таблицей, иначе будет сообщено об ошибке. Метод AutoMigrate не требует этого, потому что он рекурсивно создаёт таблицы, следуя отношениям ссылок.

Создание

Create

При создании новых записей в большинстве случаев используется метод Create

go
func (db *DB) Create(value interface{}) (tx *DB)

Дан следующий struct

go
type Person struct {
  Id   uint `gorm:"primaryKey;"`
  Name string
}

Создать запись

go
user := Person{
    Name: "jack",
}

// Должна быть передана ссылка
db = db.Create(&user)

// Ошибка, возникшая во время выполнения
err = db.Error
// Количество затронутых строк
affected := db.RowsAffected

После создания gorm запишет первичный ключ в struct user, поэтому вы должны передать указатель. Если вы передадите slice, это будет пакетное создание

go
user := []Person{
    {Name: "jack"},
    {Name: "mike"},
    {Name: "lili"},
}

db = db.Create(&user)

Аналогично, gorm запишет первичный ключ в slice. Когда объём данных слишком велик, вы также можете использовать метод CreateInBatches для пакетного создания, потому что сгенерированный SQL INSERT INTO table VALUES (),() станет очень длинным, и у каждой базы данных есть ограничения на длину SQL. Поэтому при необходимости вы можете выбрать пакетное создание.

go
db = db.CreateInBatches(&user, 50)

Кроме того, метод Save также может создавать записи. Его функция — обновлять запись при совпадении первичного ключа, иначе вставлять.

go
func (db *DB) Save(value interface{}) (tx *DB)
go
user := []Person{
    {Name: "jack"},
    {Name: "mike"},
    {Name: "lili"},
}

db = db.Save(&user)

Upsert

Метод Save может сопоставлять только первичные ключи. Мы можем построить более настраиваемый upsert, сконструировав Clause. Например, эта строка кода

go
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "name"}},
    DoNothing: false,
    DoUpdates: clause.AssignmentColumns([]string{"address"}),
    UpdateAll: false,
}).Create(&p)

Её функция: когда поле name конфликтует, обновить значение поля address. Если конфликта нет, будет создана новая запись. Вы также можете ничего не делать при конфликте

go
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "name"}},
    DoNothing: true,
}).Create(&p)

Или напрямую обновить все поля

go
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "name"}},
    UpdateAll: true,
}).Create(&p)

Перед использованием upsert не забудьте добавить индекс к полю конфликта.

Запрос

First

Для запросов gorm предоставляет довольно много методов. Первый — метод First

go
func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB)

Его функция — найти первую запись, упорядоченную по первичному ключу по возрастанию, например

go
var person Person
result := db.First(&person)
err := result.Error
affected := result.RowsAffected

Передайте указатель dest, чтобы gorm сопоставил запрошенные данные со struct.

Или используйте методы Table и Model для указания таблицы запроса. Первый принимает строковое имя таблицы, второй принимает модель сущности.

db.Table("person").Find(&p)
db.Model(Person{}).Find(&p)

TIP

Если переданный указатель содержит модель сущности, такую как указатель struct или указатель slice struct, тогда вам не нужно вручную указывать, какую таблицу запрашивать. Это правило применяется ко всем операциям CRUD.

Take

Метод Take похож на First, разница в том, что он не сортирует по первичному ключу.

go
func (db *DB) Take(dest interface{}, conds ...interface{}) (tx *DB)
go
var person Person
result := db.Take(&person)
err := result.Error
affected := result.RowsAffected

Pluck

Метод Pluck используется для пакетного запроса одного столбца из таблицы. Результаты запроса могут быть собраны в slice указанного типа, не обязательно slice типа сущности.

go
func (db *DB) Pluck(column string, dest interface{}) (tx *DB)

Например, собрать адреса всех в string slice

go
var adds []string

// SELECT `address` FROM `person` WHERE name IN ('jack','lili')
db.Model(Person{}).Where("name IN ?", []string{"jack", "lili"}).Pluck("address", &adds)

На самом деле это эквивалентно

go
db.Select("address").Where("name IN ?", []string{"jack", "lili"}).Find(&adds)

Count

Метод Count используется для подсчёта количества записей сущности

go
func (db *DB) Count(count *int64) (tx *DB)

Смотрите пример использования

go
var count int64

// SELECT count(*) FROM `person`
db.Model(Person{}).Count(&count)

Find

Наиболее часто используемый метод для пакетных запросов — метод Find

go
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB)

Он найдёт все записи, соответствующие заданным условиям

go
// SELECT * FROM `person`
var ps []Person
db.Find(&ps)

Select

По умолчанию gorm запрашивает все поля. Мы можем указать поля через метод Select

go
func (db *DB) Select(query interface{}, args ...interface{}) (tx *DB)

Например

go
// SELECT `address`,`name` FROM `person` ORDER BY `person`.`id` LIMIT 1
db.Select("address", "name").First(&p)

Эквивалентно

go
db.Select([]string{"address", "name"}).First(&p)

В то же время вы можете использовать метод Omit для игнорирования полей

go
func (db *DB) Omit(columns ...string) (tx *DB)

Например

go
// SELECT `person`.`id`,`person`.`name` FROM `person` WHERE id IN (1,2,3,4)
db.Omit("address").Where("id IN ?", []int{1, 2, 3, 4}).Find(&ps)

Поля, выбранные или проигнорированные Select и Omit, также будут действовать при создании и обновлении запросов.

Where

Условные запросы используют метод Where

go
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB)

Вот простой пример

go
var p Person

db.Where("id = ?", 1).First(&p)

Использование нескольких Where в цепочечных операциях построит несколько операторов AND, например

go
// SELECT * FROM `person` WHERE id = 1 AND name = 'jack' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).Where("name = ?", "jack").First(&p)

Или используйте метод Or для построения операторов OR

go
func (db *DB) Or(query interface{}, args ...interface{}) (tx *DB)
go
// SELECT * FROM `person` WHERE id = 1 OR name = 'jack' AND address = 'usa' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).
    Or("name = ?", "jack").
    Where("address = ?", "usa").
    First(&p)

Также есть метод Not, который похож

go
func (db *DB) Not(query interface{}, args ...interface{}) (tx *DB)
go
// SELECT * FROM `person` WHERE id = 1 OR name = 'jack' AND NOT name = 'mike' AND address = 'usa' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).
    Or("name = ?", "jack").
    Not("name = ?", "mike").
    Where("address = ?", "usa").
    First(&p)

Для условий IN вы можете напрямую передать slice в метод Where.

go
db.Where("address IN ?", []string{"cn", "us"}).Find(&ps)

Или для условий IN с несколькими столбцами вам нужно использовать тип [][]any для переноса параметров

go
// SELECT * FROM `person` WHERE (id, name, address) IN ((1,'jack','uk'),(2,'mike','usa'))
db.Where("(id, name, address) IN ?", [][]any{{1, "jack", "uk"}, {2, "mike", "usa"}}).Find(&ps)

Gorm поддерживает группировку where, которая комбинирует вышеуказанные операторы

go
db.Where(
    db.Where("name IN ?", []string{"cn", "uk"}).Where("id IN ?", []uint{1, 2}),
  ).Or(
    db.Where("name IN ?", []string{"usa", "jp"}).Where("id IN ?", []uint{3, 4}),
  ).Find(&ps)
// SELECT * FROM `person` WHERE (name IN ('cn','uk') AND id IN (1,2)) OR (name IN ('usa','jp') AND id IN (3,4))

Order

Сортировка использует метод Order

go
func (db *DB) Order(value interface{}) (tx *DB)

Смотрите пример использования

go
var ps []Person

// SELECT * FROM `person` ORDER BY name ASC, id DESC
db.Order("name ASC, id DESC").Find(&ps)

Вы также можете вызвать его несколько раз

go
// SELECT * FROM `person` ORDER BY name ASC, id DESC, address
db.Order("name ASC, id DESC").Order("address").Find(&ps)

Limit

Методы Limit и Offset часто используются для запросов с пагинацией

go
func (db *DB) Limit(limit int) (tx *DB)

func (db *DB) Offset(offset int) (tx *DB)

Вот простой пример пагинации

go
var (
    ps   []Person
    page = 2
    size = 10
)

// SELECT * FROM `person` LIMIT 10 OFFSET 10
db.Offset((page - 1) * size).Limit(size).Find(&ps)

Group

Методы Group и Having в основном используются для операций группировки

go
func (db *DB) Group(name string) (tx *DB)

func (db *DB) Having(query interface{}, args ...interface{}) (tx *DB)

Смотрите пример

go
var (
    ps []Person
)

// SELECT `address` FROM `person` GROUP BY `address` HAVING address IN ('cn','us')
db.Select("address").Group("address").Having("address IN ?", []string{"cn", "us"}).Find(&ps)

Distinct

Метод Distinct в основном используется для дедупликации

go
func (db *DB) Distinct(args ...interface{}) (tx *DB)

Смотрите пример

go
// SELECT DISTINCT `name` FROM `person` WHERE address IN ('cn','us')
db.Where("address IN ?", []string{"cn", "us"}).Distinct("name").Find(&ps)

SubQuery

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

go
// SELECT * FROM `person` WHERE id > (SELECT AVG(id) FROM `person`
db.Where("id > (?)", db.Model(Person{}).Select("AVG(id)")).Find(&ps)

Из подзапроса

go
// SELECT * FROM (SELECT * FROM `person` WHERE address IN ('cn','uk')) as p
db.Table("(?) as p", db.Model(Person{}).Where("address IN ?", []string{"cn", "uk"})).Find(&ps)

Lock

Gorm использует clause clause.Locking для поддержки блокировок

go
// SELECT * FROM `person` FOR UPDATE
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&ps)

// SELECT * FROM `person` FOR SHARE NOWAIT
db.Clauses(clause.Locking{Strength: "SHARE", Options: "NOWAIT"}).Find(&ps)

Iteration

Вы можете получить итератор через метод Rows

go
func (db *DB) Rows() (*sql.Rows, error)

Перебирая итератор, используйте метод ScanRows для сканирования результата каждой строки в struct.

go
rows, err := db.Model(Person{}).Rows()
if err != nil {
    return
}
defer rows.Close()

for rows.Next() {
    var p Person
    err := db.ScanRows(rows, &p)
    if err != nil {
        return
    }
}

Обновление

Save

Метод Save упоминался во время создания. Он также может использоваться для обновления записей, и он обновит все поля, даже если некоторые поля struct имеют нулевые значения. Однако, если первичный ключ не совпадает, он выполнит операцию вставки.

go
var p Person

db.First(&p)

p.Address = "poland"
// UPDATE `person` SET `name`='json',`address`='poland' WHERE `id` = 2
db.Save(&p)

Вы можете видеть, что он добавляет все поля, кроме первичного ключа, в оператор SET.

Update

Поэтому в большинстве случаев рекомендуется использовать метод Update

go
func (db *DB) Update(column string, value interface{}) (tx *DB)

Он в основном используется для обновления одного поля столбца

go
var p Person

db.First(&p)

// UPDATE `person` SET `address`='poland' WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Update("address", "poland")

Updates

Метод Updates используется для обновления нескольких столбцов, принимает struct и map в качестве параметров. Когда поля struct имеют нулевые значения, эти поля будут проигнорированы, но не в map.

go
func (db *DB) Updates(values interface{}) (tx *DB)

Вот пример

go
var p Person

db.First(&p)

// UPDATE `person` SET `name`='jojo',`address`='poland' WHERE `id` = 2
db.Model(p).Updates(Person{Name: "jojo", Address: "poland"})

// UPDATE `person` SET `address`='poland',`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "address": "poland"})

SQL-выражения

Иногда вам часто нужно выполнять операции над полями, такие как самоинкремент или самодекремент или другие операции, включающие само поле. Обычно вы сначала запрашиваете, затем вычисляете, затем обновляете, или используете SQL-выражения.

go
func Expr(expr string, args ...interface{}) clause.Expr

Смотрите следующий пример

go
// UPDATE `person` SET `age`=age + age,`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "age": gorm.Expr("age + age")})

// UPDATE `person` SET `age`=age * 2 + age,`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "age": gorm.Expr("age * 2 + age")})

Удаление

В gorm удаление записей использует метод Delete. Он может напрямую принимать struct сущности или передавать условия.

go
func (db *DB) Delete(value interface{}, conds ...interface{}) (tx *DB)

Например, напрямую передать struct

go
var p Person

db.First(&p)

// DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&p)

Или

go
var p Person

db.First(&p)

// DELETE FROM `person` WHERE `person`.`id` = 2
db.Model(p).Delete(nil)

Или указать условия

go
// DELETE FROM `person` WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Delete(nil)

Или упростить до

go
var p Person

db.First(&p)

// DELETE FROM `person` WHERE id = 2
db.Delete(&Person{}, "id = ?", 2)

// DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&Person{}, 2)

Для пакетного удаления передайте slice

go
// DELETE FROM `person` WHERE id IN (1,2,3)
db.Delete(&Person{}, "id IN ?", []uint{1, 2, 3})
// DELETE FROM `person` WHERE `person`.`id` IN (1,2,3)
db.Delete(&Person{}, []uint{1, 2, 3})

Мягкое удаление

Если ваша модель сущности использует мягкое удаление, во время удаления по умолчанию выполняется операция обновления. Если вы хотите навсегда удалить, вы можете использовать метод Unscoped

go
db.Unscoped().Delete(&Person{}, []uint{1, 2, 3})

Определение ассоциации

Gorm предоставляет возможности взаимодействия с ассоциациями таблиц, определяя ассоциации между struct через встроенные struct и поля.

Один-к-одному

Отношение один-к-одному самое простое. Обычно у человека может быть только одна мать. Смотрите следующий struct

go
type Person struct {
  Id      uint
  Name    string
  Address string
  Age     uint

  MomId sql.NullInt64
  Mom   Mom `gorm:"foreignKey:MomId;"`
}

type Mom struct {
  Id   uint
  Name string
}

Struct Person реализует ссылку на тип Mom путём встраивания struct Mom. Среди них Person.MomId — поле ссылки, а первичный ключ Mom.Id — ссылочное поле. Это завершает ассоциацию один-к-одному. Как настраивать внешние ключи, ссылки, ограничения и правила внешних ключей по умолчанию было объяснено в Определение внешнего ключа, поэтому не будет повторяться.

TIP

Для полей внешних ключей рекомендуется использовать типы, предоставленные пакетом sql, потому что внешние ключи по умолчанию могут быть NULL. При использовании Create для создания записей, если вы используете обычные типы, нулевое значение 0 также будет создано. Создание несуществующего внешнего ключа, очевидно, не разрешено.

Один-ко-многим

Ниже добавим struct school. Отношение между school и student — один-ко-многим. У школы много студентов, но студент может посещать только одну школу.

go
type Person struct {
    Id      uint
    Name    string
    Address string
    Age     uint

    MomId sql.NullInt64
    Mom   Mom `gorm:"foreignKey:MomId;"`

    SchoolId sql.NullInt64
    School   School `gorm:"foreignKey:SchoolId;"`
}

type Mom struct {
    Id   uint
    Name string
}


type School struct {
    Id   uint
    Name string

    Persons []Person `gorm:"foreignKey:SchoolId;"`
}

school.Persons имеет тип []Person, указывая, что он может иметь несколько студентов, в то время как Person должен содержать внешний ключ, ссылающийся на School, который является Person.SchoolId.

Многие-ко-многим

Человек может владеть многими домами, и в доме могут жить многие люди. Это отношение многие-ко-многим.

go
type Person struct {
  Id      uint
  Name    string
  Address string
  Age     uint

  MomId sql.NullInt64
  Mom   Mom `gorm:"foreignKey:MomId;"`

  SchoolId sql.NullInt64
  School   School `gorm:"foreignKey:SchoolId;"`

  Houses []House `gorm:"many2many:person_house;"`
}

type Mom struct {
  Id   uint
  Name string
}

type School struct {
  Id   uint
  Name string

  Persons []Person
}

type House struct {
  Id   uint
  Name string

  Persons []Person `gorm:"many2many:person_house;"`
}

type PersonHouse struct {
  PersonId sql.NullInt64
  Person   Person `gorm:"foreignKey:PersonId;"`
  HouseId  sql.NullInt64
  House    House `gorm:"foreignKey:HouseId;"`
}

Person и House держат типы slice друг друга для представления отношения многие-ко-многим. Отношения многие-ко-многим обычно требуют создания таблицы соединения, указанной через many2many. Внешние ключи таблицы соединения должны быть указаны правильно.

После создания struct позвольте gorm автоматически мигрировать в базу данных

go
tables := []any{
    School{},
    Mom{},
    Person{},
    House{},
    PersonHouse{},
}
for _, table := range tables {
    db.Migrator().CreateTable(&table)
}

Обратите внимание на порядок создания между ссылочными и ссылающимися таблицами.

Операции с ассоциациями

После создания вышеуказанных трёх отношений ассоциации, далее следует, как использовать ассоциации для CRUD. В основном используется метод Association

go
func (db *DB) Association(column string) *Association

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

go
db.Model(&person).Association("Mom").Find(&mom)

Например, чтобы найти мать человека через ассоциацию, параметр AssociationMom, который является именем поля Person.Mom.

Создание ассоциации

go
// Определить данные
jenny := Mom{
    Name: "jenny",
}

mit := School{
    Name:    "MIT",
    Persons: nil,
}

h1 := House{
    Id:      0,
    Name:    "h1",
    Persons: nil,
}

h2 := House{
    Name:    "h2",
    Persons: nil,
}

jack := Person{
    Name:    "jack",
    Address: "usa",
    Age:     18,
}

mike := Person{
    Name:    "mike",
    Address: "uk",
    Age:     20,
}

// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`) VALUES ('jack','usa',18,NULL,NULL)
db.Create(&jack)
// INSERT INTO `schools` (`name`) VALUES ('MIT')
db.Create(&mit)

// Добавить ассоциацию Person-Mom, ассоциация один-к-одному
// INSERT INTO `moms` (`name`) VALUES ('jenny') ON DUPLICATE KEY UPDATE `id`=`id`
// UPDATE `people` SET `mom_id`=1 WHERE `id` = 1
db.Model(&jack).Association("Mom").Append(&jenny)

// Добавить ассоциацию school-Person, ассоциация один-ко-многим
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`,`id`) VALUES ('jack','usa',18,1,1,1),('mike','uk',20,NULL,1,DEFAULT) ON DUPLICATE KEY UPDATE `school_id`=VALUES(`school_id`)
db.Model(&mit).Association("Persons").Append([]Person{jack, mike})

// Добавить ассоциацию Person-Houses, ассоциация многие-ко-многим
// INSERT INTO `houses` (`name`) VALUES ('h1'),('h2') ON DUPLICATE KEY UPDATE `id`=`id`
// INSERT INTO `person_house` (`person_id`,`house_id`) VALUES (1,1),(1,2) ON DUPLICATE KEY UPDATE `person_id`=`person_id`
db.Model(&jack).Association("Houses").Append([]House{h1, h2})

Если все записи не существуют, во время создания ассоциации сначала будут созданы записи, затем создана ассоциация.

Поиск ассоциации

Ниже демонстрируется, как искать ассоциации.

go
// Поиск ассоциации один-к-одному
var person Person
var mom Mom

// SELECT * FROM `people` ORDER BY `people`.`id` LIMIT 1
db.First(&person)
// SELECT * FROM `moms` WHERE `moms`.`id` = 1
db.Model(person).Association("Mom").Find(&mom)

// Поиск ассоциации один-ко-многим
var school School
var persons []Person

// SELECT * FROM `schools` ORDER BY `schools`.`id` LIMIT 1
db.First(&school)
// SELECT * FROM `people` WHERE `people`.`school_id` = 1
db.Model(&school).Association("Persons").Find(&persons)

// Поиск ассоциации многие-ко-многим
var houses []House

// SELECT `houses`.`id`,`houses`.`name` FROM `houses` JOIN `person_house` ON `person_house`.`house_id` = `houses`.`id` AND `person_house`.`person_id` IN (1,2)
db.Model(&persons).Association("Houses").Find(&houses)

Поиск ассоциации найдёт записи, соответствующие условиям в ссылочной таблице, на основе существующих данных. Для отношений многие-ко-многим gorm автоматически завершит процесс соединения таблиц.

Обновление ассоциации

Ниже демонстрируется, как обновлять ассоциации

go
// Обновление ассоциации один-к-одному
var jack Person

lili := Mom{
    Name: "lili",
}

// SELECT * FROM `people` WHERE name = 'jack' ORDER BY `people`.`id` LIMIT 1
db.Where("name = ?", "jack").First(&jack)

// INSERT INTO `moms` (`name`) VALUES ('lili')
db.Create(&lili)

// INSERT INTO `moms` (`name`,`id`) VALUES ('lili',2) ON DUPLICATE KEY UPDATE `id`=`id`
// UPDATE `people` SET `mom_id`=2 WHERE `id` = 1
db.Model(&jack).Association("Mom").Replace(&lili)

// Обновление ассоциации один-ко-многим

var mit School
newPerson := []Person{{Name: "bob"}, {Name: "jojo"}}
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`) VALUES ('bob','',0,NULL,NULL),('jojo','',0,NULL,NULL)
db.Create(&newPerson)

//  SELECT * FROM `schools` WHERE name = 'mit' ORDER BY `schools`.`id` LIMIT 1
db.Where("name = ?", "mit").First(&mit)

// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`,`id`) VALUES ('bob','',0,NULL,1,4),('jojo','',0,NULL,1,5) ON DUPLICATE KEY UPDATE `school_id`=VALUES(`school_id`)
//  UPDATE `people` SET `school_id`=NULL WHERE `people`.`id` NOT IN (4,5) AND `people`.`school_id` = 1
db.Model(&mit).Association("Persons").Replace(newPerson)

// Обновление ассоциации многие-ко-многим

// INSERT INTO `houses` (`name`) VALUES ('h3'),('h4'),('h5') ON DUPLICATE KEY UPDATE `id`=`id`
// INSERT INTO `person_house` (`person_id`,`house_id`) VALUES (1,3),(1,4),(1,5) ON DUPLICATE KEY UPDATE `person_id`=`person_id`
// DELETE FROM `person_house` WHERE `person_house`.`person_id` = 1 AND `person_house`.`house_id` NOT IN (3,4,5)
db.Model(&jack).Association("Houses").Replace([]House{{Name: "h3"}, {Name: "h4"}, {Name: "h5"}})

Во время обновления ассоциации, если и ссылочные данные, и ссылающиеся данные не существуют, gorm попытается создать их.

Удаление ассоциации

Ниже демонстрируется, как удалять ассоциации

go
// Удаление ассоциации один-к-одному
var (
    jack Person
    lili Mom
)

// SELECT * FROM `people` WHERE name = 'jack' ORDER BY `people`.`id` LIMIT 1
db.Where("name = ?", "jack").First(&jack)

//  SELECT * FROM `moms` WHERE name = 'lili' ORDER BY `moms`.`id` LIMIT 1
db.Where("name = ?", "lili").First(&lili)

// UPDATE `people` SET `mom_id`=NULL WHERE `people`.`id` = 1 AND `people`.`mom_id` = 2
db.Model(&jack).Association("Mom").Delete(&lili)

// Удаление ассоциации один-ко-многим

var (
    mit     School
    persons []Person
)

// SELECT * FROM `schools` WHERE name = 'mit' ORDER BY `schools`.`id` LIMIT 1
db.Where("name = ?", "mit").First(&mit)
// SELECT * FROM `people` WHERE name IN ('jack','mike')
db.Where("name IN ?", []string{"jack", "mike"}).Find(&persons)

// UPDATE `people` SET `school_id`=NULL WHERE `people`.`school_id` = 1 AND `people`.`id` IN (1,2)
db.Model(&mit).Association("Persons").Delete(&persons)

// Удаление ассоциации многие-ко-многим
var houses []House

// SELECT * FROM `houses` WHERE name IN ('h3','h4')
db.Where("name IN ?", []string{"h3", "h4"}).Find(&houses)

// DELETE FROM `person_house` WHERE `person_house`.`person_id` = 1 AND `person_house`.`house_id` IN (3,4)
db.Model(&jack).Association("Houses").Delete(&houses)

Удаление ассоциации удаляет только ссылочное отношение между ними, а не записи сущности. Мы также можем использовать метод Clear для прямой очистки ассоциации

go
db.Model(&jack).Association("Houses").Clear()

Если вы хотите удалить соответствующие записи сущности, вы можете добавить операцию Unscoped после операции Association (не повлияет на many2many)

go
db.Model(&jack).Association("Houses").Unscoped().Delete(&houses)

Для один-ко-многим и многие-ко-многим вы можете использовать операцию Select для удаления записей

go
var (
    mit     School
)
db.Where("name = ?", "mit").First(&mit)

db.Select("Persons").Delete(&mit)

Предзагрузка

Предзагрузка используется для запроса данных ассоциации. Для сущностей с отношениями ассоциации сначала будут предзагружены связанные ссылочные сущности. Запрос ассоциации, упомянутый ранее, запрашивает отношения ассоциации. Предзагрузка напрямую запрашивает записи сущности, включая все отношения ассоциации. С точки зрения синтаксиса, запрос ассоциации должен сначала запросить указанный []Person, затем запросить связанную []Mom на основе []Person. Предзагрузка, с точки зрения синтаксиса, напрямую запрашивает []Person и также загрузит все отношения ассоциации. Однако на практике SQL, который они выполняют, почти одинаков. Смотрите пример

go
var users []Person

// SELECT * FROM `moms` WHERE `moms`.`id` = 1
// SELECT * FROM `people`
db.Preload("Mom").Find(&users)

Это пример запроса ассоциации один-к-одному. Его вывод

go
[{Id:1 Name:jack Address:usa Age:18 MomId:{Int64:1 Valid:true} Mom:{Id:1 Name:jenny} SchoolId:{Int64:1 Valid:true} School:{Id:0 Name: Persons:[]} Houses:[]} {Id:2 Name:mike Address:uk Age:20 MomId:{Int64:0 Valid:false} Mom:{Id:0 Name:} SchoolId:{Int64:1 Valid:true} School:{Id:0 Name: Persons:[]} Houses:[]}]

Вы можете видеть, что связанная Mom также была запрошена, но отношение school не было предзагружено, поэтому все struct School имеют нулевые значения. Вы также можете использовать clause.Associations для указания предзагрузки всех отношений, кроме вложенных отношений.

go
db.Preload(clause.Associations).Find(&users)

Ниже пример вложенной предзагрузки. Его функция — запросить всех студентов всех ассоциаций school, мать, связанную с каждым студентом, и дома, принадлежащие каждому студенту, а также запросить набор владельцев каждого дома. School->Student->House->Student.

go
var schools []School

db.Preload("Persons").
    Preload("Persons.Mom").
    Preload("Persons.Houses").
    Preload("Persons.Houses.Persons").Find(&schools)

// Вывод кода, логику можно игнорировать
for _, school := range schools {
    fmt.Println("school", school.Name)
    for _, person := range school.Persons {
        fmt.Println("person", person.Name)
        fmt.Println("mom", person.Mom.Name)
        for _, house := range person.Houses {
            var persons []string
            for _, p := range house.Persons {
                persons = append(persons, p.Name)
            }
            fmt.Println("house", house.Name, "owner", persons)
        }
        fmt.Println()
    }
}

Вывод

school MIT
person jack
mom jenny
house h1 owner [jack]
house h2 owner [jack]

person mike
mom

Вы можете видеть, что он выводит мать каждого студента каждой школы и их дома, а также владельцев каждого дома.

Транзакции

Gorm включает транзакции по умолчанию. Любые операции вставки и обновления будут откатываться после неудачи. Вы можете отключить это в Конфигурация подключения, производительность улучшится примерно на 30%. Существует несколько методов использования транзакций в gorm. Ниже простое введение.

Автоматические

Транзакция замыкания, через метод Transaction, передайте функцию замыкания. Если возвращаемое значение функции не nil, будет автоматически выполнен откат.

go
func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error)

Смотрите пример. Операции в замыкании должны использовать параметр tx, а не внешний db.

go
var ps []Person

db.Transaction(func(tx *gorm.DB) error {
    err := tx.Create(&ps).Error
    if err != nil {
        return err
    }

    err = tx.Create(&ps).Error
    if err != nil {
        return err
    }

    err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
    if err != nil {
        return err
    }

    return nil
})

Ручные

Ручные транзакции более рекомендуются, где мы контролируем, когда выполнять откат и когда фиксировать. Ручные транзакции используют следующие три метода

go
// Метод Begin используется для запуска транзакции
func (db *DB) Begin(opts ...*sql.TxOptions) *DB

// Метод Rollback используется для отката транзакции
func (db *DB) Rollback() *DB

// Метод Commit используется для фиксации транзакции
func (db *DB) Commit() *DB

Смотрите пример. После запуска транзакции вы должны использовать tx для работы с ORM.

go
var ps []Person

tx := db.Begin()

err := tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

err = tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
if err != nil {
    tx.Rollback()
    return
}

tx.Commit()

Вы можете указать точки сохранения

go
var ps []Person

tx := db.Begin()

err := tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

tx.SavePoint("createBatch")

err = tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
if err != nil {
    tx.RollbackTo("createBatch")
    return
}

tx.Commit()

Заключение

Если вы прочитали всё вышеуказанное содержимое и набрали код, тогда вы можете использовать gorm для выполнения операций CRUD с базой данных. Помимо этих операций, gorm имеет много других функций. Для получения более подробной информации вы можете проверить официальную документацию.

Golang by www.golangdev.cn edit