Введение
Про GORM написано, что это фантастическая ORM , в чем-то она действительно фантастическая, но недостаток документации и неточности в ряде случаев заставляют изрядно «поломать голову». В данной статье я расскажу о тех проблемах, с которыми я столкнулся при реализации ряда типовых задач. Кроме того, я создал модуль (https://github.com/Wissance/gwuu), доступный на github , который позволяет упростить решение ряда задач. Но обо все по порядку.
Создание новых объектов
Мы пишем код таким образом, чтобы упростить создание новых объектов модели с использованием ORM и СУБД (автоинкремент значение primary key , значения полей по умолчанию и т.п.). GORM предлагает использовать свою структуру для того, чтобы к каждой модели присоединить идентификаторы и даты создания, обновления и удаления, т.е. это будет выглядеть следующим образом:
```
type Group struct {
gorm.Model
// other fields
}
```
Идентификатор экземпляра Group может быть получен через обращение к полю ID:
```
func CreateGroupInfoDto (model *model.Group) dto.GroupInfo {
dto := dto.GroupInfo{Id: model.ID, Name: model.Name, Description: model.Description}
return dto
}
```
Основная проблема, с которой я столкнулся, но не смог выяснить причину, так это то, что при вызове метода Save () не происходит создание объекта, при включении отладки .Debug я наблюдаю запись 0 в качестве primary key :
db .Debug .Save (&model )
К сожалению, я не смог решить эту проблему, т.к. откровенно непонятно чем она вообще вызвана (возможно она вызвана каким-то связанными сущностями). Но для того, чтобы решить эту проблему для записи объектов, использующих поле gorm .Model с целочисленными индикаторами мы можем использовать следующий подход:
1. Получить максимальное текущее значение идентификатор по данной таблице используя агрегатную функцию sql MAX (), прибавить единицу и использовать, полученное значение в качестве значения ID
2. Передать модель в метод Create
Отдельно следует упомянуть получение значения от агрегатных функции, тут есть расхождение с документацией и значение можно получить только в тип структуру (см. функцию GetNextTableId () в https ://github .com /Wissance /gwuu /blob /master /gorm /db _utils .go ) Это обобщенная функция, которая может быть использована в любых проектах для получения значения первичного ключа для любой таблицы достаточно лишь передать имя таблицы в качестве параметра. И хотя данный код доступен на github но все же я приведу кусочек кода здесь, т.к. он демонстрирует еще одну проблему
```
type Internal struct {
Id uint
}
var maxId Internal
getMaxIdQuery := stringFormatter.Format("SELECT MAX(id) As Id FROM {0};", table);
db.Raw(getMaxIdQuery).Scan(&maxId)
```
Дело в том, что я не могу использовать в качестве переменной целочисленное значение (var maxId int ) для вызова агрегатных функций через Raw () для получения значения в Scan ВСЕГДА необходимо использовать структуру. Именно это и реализовано внутри вышеупомянутой функции. Аналогично следует использовать структуру при получении среднего значения (AVG ), суммы (SUM ), минимального (MIN ), и числа строк (COUNT ).
Работа со значениями, допускающими Null
Для того, чтобы записывать NULL -значения в столбцы БД мы должны использовать типы sql .NullInt 32 и sql .NullString вместо int /uint и string в моделях.
Допустим, что поле GroupId имеют тип sql .NulInt 32, тогда при создании модели, чтобы определить Null в качестве значения достаточно не записывать в него ничего, а для записи значения отличного от Null :
```
if group.ID > 0 {
playbook.GroupId.Int32 = int32(machine.GroupId)
playbook .GroupId .Valid = true
}
```
Чуть сложнее дело обстоит при обновлении модели, предположим, что у нас было какое-то значение GroupId , но нам необходимо задать для модели NULL в качестве значения. У меня не заработал сброс в NULL через метод Updates (), поэтому мне пришлось задавать обновляемые поля модели и вызывать метод Save ():
```
var groupId sql.NullInt32
if group.ID > 0 {
groupId.Valid = true
groupId.Int32 = int32(machine.GroupId)
} else {
groupId.Int32 = 0
}
// ...
playbook.GroupId = groupId
// ...
db.Save(&playbook)
```
Особенности создания связей многие ко многим
При создании связей многие ко многим, как правило, ожидаешь, что это будет реализовано через третью таблицу (Junction ), в которой будет настроена каскадность таким образом, чтобы при удалении из любой из двух таблиц, на которые ссылается третья, происходило удаление и из Junction таблицы, но, согласно документации GORM , этого должно хватить в модели:
```
type Playbook struct {
gorm.Model
// …
Tags []Tag `gorm:"many2many:playbook_tags; constraint:OnUpdate:CASCADE, OnDelete:CASCADE;"`
// …
}
```
Однако такое определение в совокупности с использованием AutoMigrate через структуры моделей:
```
db.AutoMigrate(&Tag{})
db.AutoMigrate(&Playbook{})
```
приводили к созданию третьей таблицы, но без внешних ключей на таблицы (playbooks и tags ), данную ситуацию я решил, используя функцию Table для явного указания таблицы (хотя этот подход и не очень хорош, т.к. мы вмешиваемся в работу ORM ):
```
db.Table("playbook_tags").AddForeignKey("playbook_id", "playbooks(id)", "CASCADE", "CASCADE")
db.Table("playbook_tags").AddForeignKey("tag_id", "tags(id)", "CASCADE", "CASCADE")
```
Теперь внешние ключи создаются с правильным описанием каскадности.
Инструменты для облегчения работы с GORM и базами данных
Теперь я бы хотел перейти ко второй части данной статьи, а именно к созданию дополнительных функций, которые облегчают работу с GORM , а именно:
· Получение строки подключения к БД
· Создание БД
· Подключение к БД с предварительным созданием если БД не существует
· Удаление БД
· Постраничная выборка результатов
· Получение нового значения первичного ключа (упоминалось раньше)
Для этих целей мы (Wissance , wissance .com ) создали модуль https://github.com/Wissance/gwuu
Следует отметить, что функции по получении строки подключения, создании, открытии и удалении БД протестированы тестами для СУБД Postgres , Mysql и Mssql .
Я не буду демонстрировать отдельно каждую функцию, примеры использования отдельных из них можно посмотреть в тестах: https://github.com/Wissance/gwuu/blob/master/gorm/db_context_test.go
Я рассмотрю пример использования функций для получения строки подключения, создания и удаления БД в функциональном тесте в другом приложении. При тестировании есть парадигма AAA (Assign -Act -Assert ) при этом каждый тест приложения запускается на отдельной БД, а по завершении БД удаляется, поэтому мы получим следующий код:
```
package managers
import (
"database/sql"
"encoding/json"
"github.com/stretchr/testify/assert"
"github.com/wissance/gwuu/gorm"
"soar/stateMachineService/dto"
"soar/stateMachineService/model"
"testing"
)
func TestGetPlaybookById(t *testing.T) {
testDbName := "test_playbook_model"
connStr := gorm.BuildConnectionString(gorm.Postgres, "127.0.0.1", 5432, testDbName,
"developer", "123", "disable")
db := gorm.OpenDb2(gorm.Postgres, connStr, true)
assert.NotNil(t, db)
model.PrepareDb(db)
insertTestData(t, db)
expectedPlaybook := model.Playbook{Name: "scheduleExam", Comment: " Назначение студента на экзамен",
Script: "..\\..\\exampleMachines\\testMachine1\\scheduleExam.json"}
expectedPlaybook.ID = 1
actualPlaybook := GetPlaybookById(db, 1)
assert.NotNil(t, actualPlaybook)
checkPlaybooks(t, &expectedPlaybook, &actualPlaybook)
gorm.CloseDb(db)
gorm.DropDb(gorm.Postgres, connStr)
}
```
Как видно из примера мы сначала строим строку подключения для СУБД Postgres используя функцию BuildConnectionString , далее полученная строка подключения используется для открытия БД при этом мы создаем БД в тот момент, когда открываем и по завершении удаляем БД, в целом все довольно просто.
Далее перейдем к выборке данных порциями. К счастью GORM позволяет осуществлять такие операции используя встроенные функции и они довольно неплохо работают, в данном случае мы обобщили выборку данных с целью получить универсальный код независимо от номера страницы и размера страницы (см. функцию Paginate, https://github.com/Wissance/gwuu/blob/master/gorm/db_utils.go ). Данная функция возвращает *gorm .DB к которому применен пэйджинг, для выбора страницы данных мы должны так использовать эту функцию:
```
var playbooks []model.Playbook
db.Scopes(gorm.Paginate(page, size)).Find(&playbooks)
return playbooks
```
Заключение
В данной статье мы (Wissance , wissance .com ) рассмотрели особенности работы с GORM и несмотря на недочеты и неточности в документации данная ORM является неплохим инструментом. И если наш модуль (https://github.com/Wissance/gwuu ) оказался Вам полезен, то поставьте, пожалуйста, звездочку или напишите issue на guthub с тем, что вы бы хотели видеть еще в данной библиотеке. Спасибо, что дочитали до конца!