Одна из самых важных задач ЯП`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 есть ряд экспертных курсов по программированию. Заходите в каталог и выбирайте интересующий вас курс.
Комментарии (14)
NeoCode
18.12.2023 09:49Я пробовал, мне не понравилось. Во-первых, множество каких-то совершенно неинтуитивных функций. Во-вторых, как оказалось, gorm в некоторых случаях работает в 10 раз медленнее чем прямые sql запросы. И конструировать сложные запросы (к примеру содержащие пагинацию, фильтрацию по нескольким критириям и сортировку, причем всё опционально) куда проще с помощью обычной строки с sql кодом.
И сам gorm мне был нужен только ради автомиграции, но оказалось что и она там слишком примитивная, умеет только новые столбцы добавлять.
Нашел такую штуку как sqlx - легкая обертка над стандартным пакетом sql, умеющая связывать результаты запросов со структурами.
bogolt
18.12.2023 09:49Вместо такого
tx := db.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }()
удобнее делать так
tx := db.Begin() defer tx.Rollback()
Так мы заставляем программиста вызвать
Commit()
иначе весь код откатится вRollback()
причем не важно из-за паники или просто из-за случайногоreturn.
Snaffi
18.12.2023 09:49А ещё лучше так.
bogolt
18.12.2023 09:49Ну такое, неявный коммит как-то не очень хорошо по-моему. Я бы в коде такое не хотел. Одно дело когда у вас обертка ExecTx() и понятно что внутри будет, другое когда вы прямо в код лепите такой дефер.
Snaffi
18.12.2023 09:49Это и есть обертка
В RunTx передается функция которая выполняется в транзакции
bogolt
18.12.2023 09:49Да я понимаю. Но мой комментарий был про наружное использование defer-rollback, без оберток. В обертке понятно что коммит необходим. Я решил что вы предлагаете пихать defer commit() в обычные куски кода.
kulaginds
Удобство как есть: помимо SQL ещё нужно знать, как завернуть GORM, чтобы получить тот же SQL.
shasoftX
По идее если нормальный ORM, то SQL знать не обязательно.
kulaginds
В нашей профессии трудно устроиться на бекенд без знания SQL.
shasoftX
Разработчика GO не берут если он не знает SQL?
kulaginds
Я не видел вакансий, где для backend-разработчика не требовали знание SQL.
Snaffi
Только на джун позиции бэкэндеров
мидл и выше обязан знать SQL
Kenya
А зачем джун, если он не знает базовую вещь, как SQL? Он же все равно будет бесполезен
Snaffi
Под знанием я имею опыт эксплуатации и проектирования. У джуна его не будет по умолчанию.