Go: GORM

Теория: Условия выборки

Когда данных в таблице становится много, запрос вида «вернуть всё» быстро перестаёт быть полезным. Приложению нужны не тысячи строк сразу, а конкретные записи: пользователи старше определённого возраста, заказы по городам, события за последние дни. В SQL этим управляет оператор WHERE вместе с логическими связками AND, OR, NOT.

В GORM те же идеи выражены через методы Where(), Or(), Not() и Select(). Код остаётся на Go, а под капотом ORM собирает полный SQL-запрос.

Как GORM строит условия

GORM работает как конструктор. Каждый вызов Where() добавляет фильтр, Or() расширяет условия «или», Not() исключает значения, Select() управляет тем, какие поля попадут в результат. Вызовы можно соединять цепочкой — в финальном SQL GORM аккуратно расставит скобки, параметры и операторы.

Простейший пример: фильтр по возрасту.

var users []User

// Все пользователи старше 25 лет.
result := db.Where("age > ?", 25).Find(&users)
if result.Error != nil {
	log.Println("ошибка выборки:", result.Error)
}

log.Println("найдено записей:", result.RowsAffected)

Под капотом это превращается в:

SELECT * FROM users
WHERE age > 25;

Фильтр можно менять, не перестраивая SQL вручную — параметры подставятся безопасно.

Несколько условий: GORM сам склеит AND

Если критериев два и больше, приложение просто вызывает Where() ещё раз:

var users []User

// Старше 25 и из Москвы.
query := db.Where("age > ?", 25).Where("city = ?", "Москва")

if err := query.Find(&users).Error; err != nil {
	log.Println("ошибка выборки:", err)
}

log.Println("пользователей из Москвы старше 25:", len(users))

SQL:

WHERE age > 25 AND city = 'Москва'

Сама логика фильтра остаётся в коде Go, а про синтаксис SQL думает ORM.

Логическое «или»: Or

Or() добавляет условие, которое расширяет выборку.

var users []User

query := db.Where("city = ?", "Москва").Or("age < ?", 18)

if err := query.Find(&users).Error; err != nil {
	log.Println("ошибка выборки:", err)
}

SQL:

WHERE city = 'Москва' OR age < 18

Так удобно собирать сложные правила, не превращая SQL в нечитаемую строку.

Исключение значений: Not

Если данные нужно не включить, а исключить, подходит Not():

var users []User

// Все, кроме москвичей.
if err := db.Not("city = ?", "Москва").Find(&users).Error; err != nil {
	log.Println("ошибка выборки:", err)
}

SQL:

WHERE NOT (city = 'Москва')

Можно передать карту:

conditions := map[string]any{
	"city": []string{"Москва", "Питер"},
}

if err := db.Not(conditions).Find(&users).Error; err != nil {
	log.Println("ошибка выборки:", err)
}

SQL:

city NOT IN ('Москва', 'Питер')

Работа с полями: Select

По умолчанию GORM выбирает все столбцы. Но часто нужны только несколько полей:

var users []User

query := db.Select("name", "email").Where("age > ?", 20)

if err := query.Find(&users).Error; err != nil {
	log.Println("ошибка выборки:", err)
}

SQL:

SELECT
    name,
    email
FROM users
WHERE age > 20;

Это ускоряет запросы и уменьшает объём данных.

Опасный момент: вычисляемые поля в модель User загружать нельзя

Иногда хочется посчитать что-то «на лету»:

query := db.Select("name, age * 12 AS age_in_months").Where("age > ?", 18)

Такой запрос нельзя мапить в модель User

GORM попытается заполнять структуру согласно именам колонок. Если поле age_in_months отсутствует, значение может попасть в другие поля модели, а Age перепишется. Итог — «возраст в месяцах» тихо портит «возраст в годах».

Правильный подход — отдельная структура под результат:

type UserWithMonths struct {
	Name        string
	AgeInMonths int
}

var out []UserWithMonths

db.Model(&User{}).
	Select("name, age * 12 AS age_in_months").
	Where("age > ?", 18).
	Scan(&out)

Модель User — только для полей таблицы. Любые вычисления выводим в отдельные DTO.

Условия из карты и структуры

Where() принимает не только строки с ?, но и карты:

conditions := map[string]any{
	"city": "Москва",
	"age":  30,
}

if err := db.Where(conditions).Find(&users).Error; err != nil {
	log.Println("ошибка выборки:", err)
}

SQL:

WHERE city = 'Москва' AND age = 30

Если значение — срез, ORM строит IN. Структуры работают аналогично, но используют имена полей Go и теги gorm:"column:...".

Подводный камень: нулевые значения в структурах

Если фильтрация должна учитывать 0, пустую строку или false, структура не подходит: GORM пропускает такие поля. Именно поэтому структуры редко используют для динамических фильтров — проще карта.

Частые выражения: IN, LIKE, BETWEEN

Слетает без проблем:

cities := []string{"Москва", "Питер", "Казань"}
db.Where("city IN ?", cities).Find(&users)

Поиск подстроки:

pattern := "%ан%"
db.Where("LOWER(name) LIKE LOWER(?)", pattern).Find(&users)

Диапазоны:

db.Where("age BETWEEN ? AND ?", 18, 30).Find(&users)

Все параметры передаются безопасно — никакой конкатенации строк.

Что точно не нужно делать

Плохой вариант:

"age > " + strconv.Itoa(age)

Такие строки:

  • плохо читаются,
  • легко ломаются,
  • небезопасны (SQL-инъекции).

Правильный способ — WHERE age > ? или карта условий.

Как GORM собирает запрос внутри

Все вызовы Where(), Or(), Not(), Select() попадают в объект Statement. Перед выполнением GORM:

  1. собирает условия в единый SQL-текст,
  2. подставляет ?,
  3. передаёт параметры отдельно в driver.

Код остаётся декларативным, а ORM собирает корректный SQL за вас.

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845