Одна из самых важных задач ЯП`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)


  1. kulaginds
    18.12.2023 09:49

    Удобство как есть: помимо SQL ещё нужно знать, как завернуть GORM, чтобы получить тот же SQL.


    1. shasoftX
      18.12.2023 09:49

      По идее если нормальный ORM, то SQL знать не обязательно.


      1. kulaginds
        18.12.2023 09:49

        В нашей профессии трудно устроиться на бекенд без знания SQL.


        1. shasoftX
          18.12.2023 09:49

          Разработчика GO не берут если он не знает SQL?


          1. kulaginds
            18.12.2023 09:49

            Я не видел вакансий, где для backend-разработчика не требовали знание SQL.


          1. Snaffi
            18.12.2023 09:49

            Только на джун позиции бэкэндеров

            мидл и выше обязан знать SQL


            1. Kenya
              18.12.2023 09:49

              А зачем джун, если он не знает базовую вещь, как SQL? Он же все равно будет бесполезен


              1. Snaffi
                18.12.2023 09:49

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


  1. NeoCode
    18.12.2023 09:49

    Я пробовал, мне не понравилось. Во-первых, множество каких-то совершенно неинтуитивных функций. Во-вторых, как оказалось, gorm в некоторых случаях работает в 10 раз медленнее чем прямые sql запросы. И конструировать сложные запросы (к примеру содержащие пагинацию, фильтрацию по нескольким критириям и сортировку, причем всё опционально) куда проще с помощью обычной строки с sql кодом.

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

    Нашел такую штуку как sqlx - легкая обертка над стандартным пакетом sql, умеющая связывать результаты запросов со структурами.


  1. 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.


    1. Snaffi
      18.12.2023 09:49

      1. bogolt
        18.12.2023 09:49

        Ну такое, неявный коммит как-то не очень хорошо по-моему. Я бы в коде такое не хотел. Одно дело когда у вас обертка ExecTx() и понятно что внутри будет, другое когда вы прямо в код лепите такой дефер.


        1. Snaffi
          18.12.2023 09:49

          Это и есть обертка

          В RunTx передается функция которая выполняется в транзакции


          1. bogolt
            18.12.2023 09:49

            Да я понимаю. Но мой комментарий был про наружное использование defer-rollback, без оберток. В обертке понятно что коммит необходим. Я решил что вы предлагаете пихать defer commit() в обычные куски кода.