Object-Relational Mapping (ORM) в Golang
Одна из самых важных задач ЯП`s это эффективное взаимодействие с базами данных и Go не исключение. В Go есть парадигма Object-Relational Mapping (ORM), позволяет работать с реляционными базами данных в терминах объектно-ориентированного программирования. Это очень сильно упрощает работу с базами данных, позволяя сосредоточиться на бизнес-логике приложения, а не на нюансах SQL-запросов.
GORM (Go Object-Relational Mapping) предоставляет удобный интерфейс для взаимодействия с различными базами данных, сохраняя при этом идиоматичность и фичу конкуретности в Go.
Установка и настройка GORM
Естественно, для работы необходим непосредственно сам go: d.
GORM поддерживает множество БД: MySQL, PostgreSQL, SQLite. Настройка подключения зависит от выбранной вами СУБД.
Для MySQL:
import (
"gorm.io/gorm"
"gorm.io/driver/mysql"
)
func main() {
dsn := "user:password@tcp(localhost:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}
Для PostgreSQL:
import (
"gorm.io/gorm"
"gorm.io/driver/postgres"
)
func main() {
dsn := "host=localhost user=user dbname=db password=password sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
}
Для SQLite:
import (
"gorm.io/gorm"
"gorm.io/driver/sqlite"
)
func main() {
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
}
После успешного коннетка к БД, вы можете начать определять модели и работать с ними. Например, создание модели User
и выполнение миграций выглядит так:
type User struct {
gorm.Model
Name string
Email string `gorm:"uniqueIndex"`
}
func main() {
// Подключение к базе данных...
db.AutoMigrate(&User{})
// Дальнейшие операции с базой данных
}
Код автоматически создаст таблицу users
в вашей базе данных, если она еще не существует. А про миграцию чуть попозже.
Определение моделей в GORM
В GORM, каждая структура Go (struct
) представляет собой таблицу в базе данных. Поля структуры соответствуют столбцам в таблице.
Теги GORM используются для указания дополнительных деталей о полях структуры, таких как ограничения, индексы и типы данных.
type User struct {
gorm.Model
Name string `gorm:"size:255"`
Email string `gorm:"type:varchar(100);unique_index"`
Age int `gorm:"default:18"`
}
gorm.Model
добавляет стандартные поля ID
, CreatedAt
, UpdatedAt
, DeletedAt
. size:255
и type:varchar(100)
определяют типы данных и размеры столбцов. unique_index
создает уникальный индекс для поля Email
.
GORM автоматически рассматривает поле с именем ID
как первичный ключ. Можно определить другой первичный ключ с помощью тега primaryKey
.
GORM поддерживает определение отношений между таблицами, таких как hasOne
, hasMany
, belongsTo
, many2many
. Пример:
type User struct {
gorm.Model
CreditCards []CreditCard `gorm:"foreignKey:UserID"`
}
type CreditCard struct {
gorm.Model
Number string
UserID uint
}
CreditCards
в User
определяет отношение один ко многим с таблицей CreditCard
.
Можно встраивать одну структуру в другую. Это полезно для повторного использования общих полей.
Если вы хотите использовать специфическое имя таблицы вместо стандартного (которое GORM генерирует из имени структуры), можно определить метод TableName
.
GORM облегчает миграцию схемы базы данных с использованием функции AutoMigrate
.
GORM позволяет определить пользовательские типы данных, которые могут быть преобразованы из Go типов в типы, понятные базе данных.
Можно использовать условные теги для определения полей, которые должны присутствовать только при определенных условиях:
primaryKey
: указывает, что поле является первичным ключом:
ID uint `gorm:"primaryKey"`
unique
: создает уникальный индекс для поля, обеспечивая уникальность значений в нем:
Email string `gorm:"unique"`
size
: определяет максимальный размер поля (для строк):
Name string `gorm:"size:255"`
type
: указывает конкретный тип данных для поля в базе данных:
Description string `gorm:"type:text"`
index
: создает индекс для поля или группы полей:
CreatedAt time.Time `gorm:"index"`
index:idx_name,sort:desc
: создает индекс с указанным именем и порядком сортировки:
Age int `gorm:"index:idx_age,sort:desc"`
Определение индекса для нескольких полей:
goCopy codetype Product struct {
ID uint
Name string `gorm:"index:idx_name_code"`
Code string `gorm:"index:idx_name_code"`
}
default
: устанавливает значение по умолчанию для поля.
goCopy codeActive bool `gorm:"default:true"`
Неявные индексы (Implicit Indexes): GORM автоматически создает индексы для полей, отмеченных как unique
и primaryKey
CRUDи прочие операции
Создание
Операция создания в GORM включает добавление новых записей в базу данных. Процесс начинается с создания экземпляра модели и последующего использования метода Create
.
user := User{Name: "John Doe", Age: 30}
result := db.Create(&user) // создает новую запись в базе данных
if result.Error != nil {
// обработка ошибки создания
}
Чтение
Операции чтения включают извлечение данных из базы данных. GORM предоставляет различные методы для этого, включая First
, Find
, Take
.
Использование
First
для получения первой записи:var user User db.First(&user)
Использование
Find
для получения всех записей:var users []User db.Find(&users)
Использование
Take
для получения одной записи:var user User db.Take(&user)
Обновление
Обновление включает изменение существующих записей в базе данных. Методы Save
и Updates
обычно используются для этой цели.
Обновление всех полей записи:
db.Model(&user).Updates(User{Name: "Perry Utkonos", Age: 25})
Частичное обновление полей записи:
db.Model(&user).Updates(map[string]interface{}{"Name": "Perry Utkonos", "Age": 25})
Удаление
Удаление удаляет записи из базы данных. Метод Delete
используется для удаления одной или нескольких записей.
Пример:
db.Delete(&user, 1) // Удаляет запись с ID 1
В дополнение к базовым CRUD-операциям, GORM позволяет выполнить ряд продвинутых действий, таких как:
Пакетные операции
Пакетные операции в GORM позволяют обрабатывать большие объемы данных, выполняя массовое добавление, обновление или удаление записей:
Массовое добавление: позволяет добавить множество записей одним запросом, к примеру:
var users []User
// Добавление данных в users
db.Create(&users)
Массовое обновление: обновление множества записей на основе определенных критериев:
db.Model(&User{}).Where("active = ?", true).Updates(map[string]interface{}{"active": false})
Транзакции
Если одна из операций в транзакции не удается, вся транзакция откатывается, что гарантирует целостность данных.
К примеру транзакция может выглядеть так:
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if tx.Error != nil {
return // Возвращает ошибку при неудачной инициализации транзакции
}
// Последовательность операций
if err := tx.Create(&newUser).Error; err != nil {
tx.Rollback()
return
}
if err := tx.Update(&existingUser).Error; err != nil {
tx.Rollback()
return
}
tx.Commit()
Предварительная загрузка
Предварительная загрузка в GORM позволяет автоматически загружать связанные данные, предотвращая проблему N+1 запросов, когда для каждой записи выполняется отдельный запрос для загрузки связанных данных.
Допустим, у нас есть модели User
и Profile
. Мы хотим загрузить пользователей вместе с их профилями:
var users []User
db.Preload("Profile").Find(&users)
Preload
автоматически загружает связанные данные для каждого пользователя, уменьшая общее количество запросов к базе данных.
Ассоциации и отношения в моделях
«Has One» ассоциация: Это тип отношения, где одна модель явно связана с одной экземпляром другой модели. Например, если у нас есть модель User
и Profile
, каждый пользователь может иметь только один профиль. В GORM это отношение определяется путем добавления поля Profile
в структуру User
, и GORM автоматически управляет связью между этими моделями.
Реализуем в коде:
type User struct {
gorm.Model
Name string
Profile Profile
}
type Profile struct {
gorm.Model
UserID uint
Bio string
}
// Создание пользователя с профилем
user := User{Name: "Perry Utkonos", Profile: Profile{Bio: "strange utkonos"}}
db.Create(&user)
«Has Many» ассоциация: Этот тип отношения используется, когда одна модель связана с несколькими экземплярами другой модели. Например, модель User
может иметь несколько Orders
. В этом случае в структуре User
будет поле, представляющее собой срез экземпляров Order
. GORM обеспечивает удобный доступ к этим связанным данным и позволяет легко выполнять операции с ними.
В коде:
type User struct {
gorm.Model
Name string
Orders []Order
}
type Order struct {
gorm.Model
UserID uint
Item string
}
// добавление заказов пользователю
var orders = []Order{{Item: "Item 1"}, {Item: "Item 2"}}
user := User{Name: "Jane Doe", Orders: orders}
db.Create(&user)
«Belongs To» ассоциация: Это обратная сторона отношения «Has One» и «Has Many». В этом случае подчиненная модель содержит ссылку на родительскую модель. Например, если у нас есть Order
, который принадлежит User
, в структуре Order
будет поле, указывающее на User
. Это позволяет легко навигировать от подчиненной модели к родительской.
В коде:
type User struct {
gorm.Model
Name string
}
type Order struct {
gorm.Model
Item string
UserID uint
User User
}
// создание заказа для пользователя
user := User{Name: "Alice"}
db.Create(&user)
order := Order{Item: "Book", UserID: user.ID}
db.Create(&order)
«Many-to-Many» ассоциации: Этот тип отношения используется, когда несколько экземпляров одной модели связаны с несколькими экземплярами другой модели. Например, если у нас есть модели Student
и Course
, студент может посещать несколько курсов, и на каждом курсе может быть несколько студентов. В GORM для управления этими отношениями используется промежуточная таблица, которая связывает экземпляры двух моделей:
type Student struct {
gorm.Model
Name string
Courses []Course `gorm:"many2many:student_courses;"`
}
type Course struct {
gorm.Model
Title string
Students []Student `gorm:"many2many:student_courses;"`
}
course := Course{Title: "Biology"}
student := Student{Name: "Bob"}
db.Create(&course)
db.Create(&student)
db.Model(&student).Association("Courses").Append(&course)
Другие возможности Gorm
Работа с транзакциями
Транзакции позволяют управлять группой операций с базой данных как единым целым, гарантируя, что либо все операции выполнятся успешно, либо, в случае ошибки, ни одна из них не будет применена.
tx := db.Begin()
// Выполнение операций в рамках транзакции
if err := tx.Create(&newRecord).Error; err != nil {
tx.Rollback()
return err
}
// Подтверждение транзакции
tx.Commit()
Хуки
GORM предоставляет хуки (callbacks), которые позволяют вмешаться в процесс выполнения операций, таких как добавление, обновление или удаление записей.
К примеру можно определить функции, которые будут вызываться до или после определенных операций (например, BeforeCreate
, AfterCreate
).
Области запросов (Scopes)
Области запросов позволяют переиспользовать определенные условия запросов, делая код чище и более поддерживаемым.
func ActiveUsers(db *gorm.DB) *gorm.DB {
return db.Where("active = ?", true)
}
db.Scopes(ActiveUsers).Find(&users)
Построители запросов
Также в GORM доступные стандартные sql запросы с условиями, группировками, сортировками и так далее.
Использование методов Where
, Order
, Limit
для формирования запросов.
Пагинация и сортировка данных
Пагинация и сортировка мастхев для управления большими наборами данных, особенно при разработке веб-приложений, к примеру:
package main
import (
"gorm.io/gorm"
"gorm.io/driver/sqlite"
"log"
)
type Product struct {
gorm.Model
Code string
Price uint
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal("failed to connect database")
}
// автлматическая миграция схемы
db.AutoMigrate(&Product{})
// Пагинация и сортировка
var products []Product
pageSize := 10 // Количество записей на страницу
page := 1 // Номер страницы
db.Order("price desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&products)
// products теперь содержит страницу продуктов, отсортированных по убыванию цены
}
Миграции
Функция AutoMigrate
автоматизирует процесс создания и обновления таблиц, основанных на структурах Go.
AutoMigrate
принимает структуры Go и создает или обновляет соответствующие таблицы в базе данных. Каждое поле в структуре Go интерпретируется как столбец в таблице.
Если таблица, соответствующая структуре, не существует в базе данных, AutoMigrate
создает её. Это включает в себя определение всех столбцов согласно полям структуры и их типам.
Если таблица уже существует, AutoMigrate
проверяет структуру таблицы и обновляет её, чтобы соответствовать структуре Go. Это может включать добавление новых столбцов или изменение существующих. Однако, AutoMigrate
не удаляет столбцы и не изменяет типы данных существующих столбцов.
AutoMigrate
также управляет созданием индексов и ограничений на основе тегов, определенных в структурах Go.
Пример:
// Автоматическая миграция схемы
db.AutoMigrate(&Product{})
// Пагинация и сортировка
var products []Product
pageSize := 10 // Количество записей на страницу
page := 1 // Номер страницы
db.Order("price desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&products)
// products теперь содержит страницу продуктов, отсортированных по убыванию цены
В кодеAutoMigrate
будет использоваться для создания или обновления таблицы users
в базе данных SQLite. Таблица будет содержать столбцы id
, created_at
, updated_at
, deleted_at
(предоставлены gorm.Model
), name
и email
, причем для email
будет создан уникальный индекс.
AutoMigrate
не удаляет столбцы и не изменяет типы данных существующих столбцов. Это предотвращает потенциальную потерю данных. Использование AutoMigrate
в продакшен-среде требует осторожности, поскольку неожиданные изменения схемы могут привести к проблемам.
Конечно, существуют еще прочие возможности для работы с БД с go, к примеру SQLC, SQLX, Beego, GORP, Go-firestorm и SQLBoile. Однако GORM в целом имеет больше возможностей :).
В завершение хочу напомнить о том, что у моих коллег из OTUS есть ряд экспертных курсов по программированию. Заходите в каталог и выбирайте интересующий вас курс.