lesson04.md 12 KB

GORM

GORM -- golang object relation model. Библиотека для работы с реляционными БД.

gorm.io

Что умеет

  • построение моделей и автомиграция БД;
  • все виды запросов;
  • атрибуты полей таблиц;
  • перехватичики (хуки);
  • ограничения и индексы;
  • несколько драйверов к разным базам данных;
  • упреждающая загрузка;
  • транзакции и многое другое.

Поддержка БД

  • MySQL;
  • MariaDB;
  • PostgreSQL;
  • SQLite;
  • SQL Server;
  • TiDB;
  • Clickhouse;
  • другие по реализации интерфейса.

Пример модели

package main

import (
  "gorm.io/gorm"
  "gorm.io/driver/sqlite"
)

type Product struct {
    gorm.Model
    Code  string
    Price uint
}

func main() {
    db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    if err != nil {
        panic("при подключении к БД")
    }

    // Автомиграция схемы
    db.AutoMigrate(&Product{})

    // Создание записи в таблице
    db.Create(&Product{Code: "D42", Price: 100})

    // Чтение записи из таблицы
    var product Product
    db.First(&product, 1) // Поиск продукта по первичному ключу
    db.First(&product, "code = ?", "D42") // Поиск продукта по коду D42
}

Модель GORM

// Определение gorm.Model
type Model struct {
    ID        uint           `gorm:"primaryKey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt gorm.DeletedAt `gorm:"index"`
}

Теги полей

Список далеко не полный:

  • column column db name
  • type column data type, prefer to use compatible general type, e.g: bool, int, uint, float, string, time, bytes, which works for all databases, and can be used with other tags together, like not null, size, autoIncrement… specified database data type like varbinary(8) also supported, when using specified database data type, it needs to be a full database data type, for example: MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT
  • serializer specifies serializer for how to serialize and deserialize data into db, e.g: serializer:json/gob/unixtime
  • size specifies column data size/length, e.g: size:256
  • primaryKey specifies column as primary key
  • unique specifies column as unique
  • default specifies column default value

. . .

Допустимость операций над полем

  • <- set field’s write permission,
    • <-:createcreate-only field,
    • <-:update update-only field,
    • <-:falseno write permission,
    • <- create and update permission
  • -> set field’s read permission,
    • ->:false no read permission
  • - ignore this field,
    • - no read/write permission,
    • -:migration no migrate permission,
    • -:all no read/write/migrate permission
    • comment add comment for field when migration

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

user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}

result := db.Create(&user) // ОБЯЗАТЕЛЬНО передавать ссылку на Create

users := []*User{
  User{Name: "Jinzhu", Age: 18, Birthday: time.Now()},
  User{Name: "Jackson", Age: 19, Birthday: time.Now()},
}

result := db.Create(users) // Передача нескольких записей сразу

// Создать только с указанными полями
db.Select("Name", "Age", "CreatedAt").Create(&user)

// Инорировать при создании указанные поля
db.Omit("Name", "Age", "CreatedAt").Create(&user)

Более сложный пример при вставке с решением конфликтов (и др.) есть в документации.

Получение записи


// Получить первую запись
db.First(&user)

// Получить последнюю запись
db.Last(&user)

// Получить по индексу
db.First(&user, 10)

// Получить несколько по указанным индексам в список
var users []User
db.Find(&users, []int{1,2,3})

// Сделать выборку по условию
db.Where("name <> ?", "jinzhu").Find(&users)
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
db.Where("name LIKE ?", "%jin%").Find(&users)
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// С доаолнительным условием
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)

// По указанным полям
db.Select("name", "age").Find(&users)

// Отсортировать по полям
db.Order("age desc, name").Find(&users)

// Ограничить тремя записями
db.Limit(3).Find(&users)

// Ограничить 10 записей со смещением 5
db.Limit(10).Offset(5).Find(&users)

Также можно объединять данные из разных таблиц

type result struct {
  Name  string
  Email string
}

// Из таблицы users выбрать имя, из таблицы emails выбрать email, где user.id совпал
db.Model(&User{}).Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&result{})
// SELECT users.name, emails.email FROM `users` left join emails on emails.user_id = users.id

// Решение объединения данных с предзагрузкой данных
db.Joins("Company").Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id`;

// Внутреннее объединение
db.InnerJoins("Company").Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name`

Есть гораздо более сложные примеры, можно найти в документации

Цикл по записям


rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Rows()
defer rows.Close()

for rows.Next() {
    var user User
    // ScanRows метод в `gorm.DB`, может быть использован для сканирования записей
    db.ScanRows(rows, &user)

    // Что-то делаем
}

// работа почками записей по 100 штук
result := db.Where("processed = ?", false).FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
    for _, result := range results {
        // пакетная обработка найденных записей
    }

    tx.Save(&results)

    // tx.RowsAffected // число записей в этой пачке

    // batch // Пачка 1, 2, 3

    // returns error will stop future batches
    return nil
})

Перехватчики/хуки

func (u *User) AfterFind(tx *gorm.DB) (err error) {
  if u.Role == "" {
    u.Role = "user"
  }
  return
}

Обновление записей


db.First(&user)

user.Name = "jinzhu 2"
user.Age = 100
db.Save(&user)

// Принудительное обновление поля
db.Save(&User{Name: "jinzhu", Age: 100})

// Обновление по условию
db.Model(&user).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;


// Обновление нескольких полей
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})

// Обновление только указанных полей (в SELECT)
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})

// Обновление с подзапросом (обновить имя копании у пользователя)
db.Table("users as u").Where("name = ?", user.Name).Update("company_name", db.Table("companies as c").Select("name").Where("c.id = u.company_id"))

Перехватчик перед обновлением:

func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
  if u.Role == "admin" {
    return errors.New("admin user not allowed to update")
  }
  return
}

Удаление записи

// Простое удаление. где ID=`10` (уже в структуре)
db.Delete(&email)

// Удаление с условием
db.Where("name = ?", user.Name).Delete(&email)

// Удаление с явным указанием ID
db.Delete(&User{}, 10)

// Удаление пачкой
var users = []User{{ID: 1}, {ID: 2}, {ID: 3}}
db.Delete(&user

// Удаление пачкой
db.Where("age = ?", 20).Delete(&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;

// Физическое удаление
db.Unscoped().Delete(&order)
// DELETE FROM orders WHERE id=10

Перехватчик перед удалением:

func (u *User) BeforeDelete(tx *gorm.DB) (err error) {
  if u.Role == "admin" {
    return errors.New("admin user not allowed to delete")
  }
  return
}

Встроенные функции

db.Table("deleted_users").Count(&count)
// SELECT count(1) FROM deleted_users;

Сырые запросы

type Result struct {
  ID   int
  Name string
  Age  int
}

// Выборка по условию
var result Result
db.Raw("SELECT id, name, age FROM users WHERE id = ?", 3).Scan(&result)

// Выполнить что-то на севере по условию
db.Exec("UPDATE orders SET shipped_at = ? WHERE id IN ?", time.Now(), []int64{1, 2, 3})

// Посмотреть какой запрос сгенерирован
sql := db.ToSQL(func(tx *gorm.DB) *gorm.DB {
  return tx.Model(&User{}).Where("id = ?", 100).Limit(10).Order("age desc").Find(&[]User{})
})
// SELECT * FROM "users" WHERE id = 100 AND "users"."deleted_at" IS NULL ORDER BY age desc LIMIT 10

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

type User struct {
  gorm.Model
  Username string
  Orders   []Order
}

type Order struct {
  gorm.Model
  UserID uint
  Price  float64
}

// Прямая предзагрузка
db.Preload("Orders").Find(&users)

// Предзагрузка с помощью левого соединения
db.Joins("Company").Joins("Manager").Joins("Account").Find(&users, "users.id IN ?", []int{1,2,3,4,5})

// Предзагрузка с условиями
db.Where("state = ?", "active").Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)

Использование контекста

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

db.WithContext(ctx).Find(&users)

Автомиграция

У автоиграции масса методов, все можно найти в документации.

Ниже только часть из них:

// Создать таблицу, если не существует (самое простое)
db.AutoMigrate(&User{})

// Автомиграция по нескольким таблицам сразу (самое простое)
db.AutoMigrate(&User{}, &Product{}, &Order{})

// Создать таблицу для структуры `User`
db.Migrator().CreateTable(&User{})

// Переименовать таблицу
db.Migrator().RenameTable(&User{}, &UserInfo{})

// Добавить колонку
db.Migrator().AddColumn(&User{}, "Name")

// Удалить колонку
db.Migrator().DropColumn(&User{}, "Name")

Отображения (views)

// Запрос на создание отображения
query := db.Model(&User{}).Where("age > ?", 20)
db.Migrator().CreateView("users_pets", gorm.ViewOption{Query: query})
db.Migrator().DropView("users_pets")

Пользовательские типы

type User struct {
  gorm.Model
  Name string
}

// SetName -- устанавливает имя пользователя
func (sf *User)SetName(name string){
  sf.Name = name
  db.Model(sf).Update("name", sf.Name)
}