# GORM **GORM** -- golang object relation model. Библиотека для работы с реляционными БД. [gorm.io](https://gorm.io/) ## Что умеет - построение моделей и автомиграция БД; - все виды запросов; - атрибуты полей таблиц; - перехватичики (хуки); - ограничения и индексы; - несколько драйверов к разным базам данных; - упреждающая загрузка; - транзакции и многое другое. ## Поддержка БД - MySQL; - MariaDB; - PostgreSQL; - SQLite; - SQL Server; - TiDB; - Clickhouse; - другие по реализации интерфейса. ## Пример модели ```golang 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 ```golang // Определение 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, - `<-:create `create-only field, - `<-:update` update-only field, - `<-:false `no 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 ## Создание записи ```golang 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) ``` Более сложный пример при вставке с решением конфликтов (и др.) есть в документации. ## Получение записи ```golang // Получить первую запись 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) ``` Также можно объединять данные из разных таблиц ```golang 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` ``` Есть гораздо более сложные примеры, можно найти в документации ## Цикл по записям ```golang 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 }) ``` ## Перехватчики/хуки ```golang func (u *User) AfterFind(tx *gorm.DB) (err error) { if u.Role == "" { u.Role = "user" } return } ``` ## Обновление записей ```golang 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")) ``` Перехватчик перед обновлением: ```golang func (u *User) BeforeUpdate(tx *gorm.DB) (err error) { if u.Role == "admin" { return errors.New("admin user not allowed to update") } return } ``` ## Удаление записи ```golang // Простое удаление. где 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 ``` Перехватчик перед удалением: ```golang func (u *User) BeforeDelete(tx *gorm.DB) (err error) { if u.Role == "admin" { return errors.New("admin user not allowed to delete") } return } ``` ## Встроенные функции ```golang db.Table("deleted_users").Count(&count) // SELECT count(1) FROM deleted_users; ``` ## Сырые запросы ```golang 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 ``` ## Предзагрузка ```golang 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) ``` ## Использование контекста ```golang ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() db.WithContext(ctx).Find(&users) ``` ## Автомиграция У автоиграции масса методов, все можно найти в документации. Ниже только часть из них: ```golang // Создать таблицу, если не существует (самое простое) 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) ```golang // Запрос на создание отображения query := db.Model(&User{}).Where("age > ?", 20) db.Migrator().CreateView("users_pets", gorm.ViewOption{Query: query}) db.Migrator().DropView("users_pets") ``` ## Пользовательские типы ```golang type User struct { gorm.Model Name string } // SetName -- устанавливает имя пользователя func (sf *User)SetName(name string){ sf.Name = name db.Model(sf).Update("name", sf.Name) } ```