Найти тему
Wissance

Особенности работы с GORM

Оглавление
В данной статье мы расскажем как побороть проблемы, баги, а также внесем больше ясности в описание работы с GORM
В данной статье мы расскажем как побороть проблемы, баги, а также внесем больше ясности в описание работы с GORM

Введение

Про 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 с тем, что вы бы хотели видеть еще в данной библиотеке. Спасибо, что дочитали до конца!