Содержание урока · 13 разделов
- gorm update: Update — одно поле, WHERE и след
- Updates: структура против карты
- Select и Omit: состав SET под контролем
- RowsAffected: сколько строк реально тронуто
- UpdateColumn и UpdateColumns: SkipHooks из finisher_api.go
- gorm.Expr: атомарные инкременты без гонки
- Конкурентные правки: оптимистичная блокировка через версию
- Пессимистичная блокировка: SELECT FOR UPDATE
- Шпаргалка: методы обновления
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
gorm update: Update — одно поле, WHERE и след#
Сигнатура из finisher_api.go лаконична: func (db DB) Update(column string, value interface{}) (tx DB). Внутри метод кладёт в Statement.Dest карту map[string]interface{}{column: value} и запускает цепочку колбэков Update — отсюда все поведения, о которых пойдёт речь: WHERE из модели, автоправка updated_at, защита от глобальных правок. WHERE берётся из двух мест, и полезно понимать, какое сработало. Первое — первичный ключ структуры в Model(&task): после чтения из базы task.ID = 3, и при обновлении GORM сам превратит ключ в условие WHERE id = 3 — механика ConvertToAssignments в callbacks/update.go выносит первичные ключи обновляемой модели из SET в WHERE. Второе — явный Where: db.Model(&TodoItem{}).Where("status = ?", "todo").Update(...) — формы условий ровно те, что разобраны седьмым уроком, ничего нового придумывать не нужно. Если модель в Model без ключа и Where нет — сработает защита ErrMissingWhereClause, ей посвящён отдельный раздел ниже. Кто ищет update gorm в поиске, хочет именно эту пару: Update по колонке плюс условие из Where. Одна колонка за вызов — как только полей становится два, дорога ведёт к Updates, следующему разделу.
Update и его SQL-след
GO// Читаем задачу 3 «Ответить заказчику», она в статусе doing
var task TodoItem
db.First(&task, 3)
// WHERE из первичного ключа: GORM сам превратил task.ID в условие
db.Model(&task).Update("status", "done")
// UPDATE "todo_items" SET "status"='done',"updated_at"='...' WHERE id = 3
// WHERE явный: формы условий - те же, что разобраны седьмым уроком
db.Model(&TodoItem{}).Where("status = ?", "todo").Update("priority", 2)
// UPDATE "todo_items" SET "priority"=2,"updated_at"='...' WHERE status = 'todo'
// Батч-обновление по списку ключей
db.Model(&TodoItem{}).Where("id IN ?", []uint{2, 4}).Update("priority", 3)
// UPDATE "todo_items" SET "priority"=3,"updated_at"='...' WHERE id IN (2,4)Первый вызов — самый частый в живых сервисах: модель прочитана из базы, вы правите одно поле, ключ из структуры уезжает в WHERE автоматически, руками писать Where("id = ?", task.ID) не нужно. Во втором и третьем вызовах Model(&TodoItem{}) — модель без ключа: она нужна, чтобы GORM знал таблицу и схему, а условие приходит целиком из Where. Во всех трёх следах видно подмену updated_at — её механика ниже в уроке.
Проверка: откуда берётся WHERE
task прочитан из базы и имеет ID = 3. Что сделает db.Model(&task).Update("status", "done")?
Updates: структура против карты#
Сигнатура соседа: func (db DB) Updates(values interface{}) (tx DB), а комментарий в finisher_api.go уточняет: values must be a struct or map — третьего не дано. Структура — продолжение знакомой истории: обновление собирается только из ненулевых полей, то же isZero-правило, что и в структурных условиях седьмого урока, работает и здесь. Задали Done: false — поле молча выпало из SET. Не ошибка, а самая дорогая тишина в CRUD: запрос выглядит успешным, значение не тронуто, заметите вы это по косвенным признакам. Зато каждая ненулевая колонка типизирована компилятором: опечатка в имени поля ловится на этапе сборки, а не в продакшене. Карта — противоположный характер: едут все ключи, включая нули, потому что карта не умеет отличать «пусто» от «не заполнял»; это путь к честным done = false и priority = 0. Две детали механики из callbacks/update.go. Первая: ключи карты GORM сортирует — sort.Strings над списком ключей, поэтому колонки в SET идут по алфавиту и SQL-след стабилен от запуска к запуску. Вторая: updated_at дописывается сам, если его нет среди ключей карты или полей структуры — разговор о времени ниже, в разделе про UpdateColumn. Как выбирать: типобезопасные частичные правки — структурой; нулевые значения и динамический состав из запросов API — картой.
Updates: структура, карта, Select, Omit
GO// Структура: только ненулевые поля; Done: false в SET не поедет
db.Model(&task).Updates(TodoItem{Status: "done", Priority: 5})
// UPDATE ... SET "priority"=5,"status"='done',"updated_at"='...' WHERE id = 3
// Карта: нули уважаются, ключи в SET идут по алфавиту
db.Model(&task).Updates(map[string]interface{}{"done": false, "priority": 0})
// UPDATE ... SET "done"=false,"priority"=0,"updated_at"='...' WHERE id = 3
// Select форсирует перечисленное поле структуры, даже нулевое
db.Model(&task).Select("Done").Updates(TodoItem{Done: false})
// UPDATE ... SET "done"=false,"updated_at"='...' WHERE id = 3
// Omit вычёркивает колонку из обновления
db.Model(&task).Omit("priority").Updates(TodoItem{Status: "done", Priority: 5})
// UPDATE ... SET "status"='done',"updated_at"='...' - priority не тронутСравните первый и второй следы: одна и та же мысль «закрыть задачу и обнулить приоритет» структурой получилась только наполовину. Select("Done") — лечение в структурной форме: перечисленное поле едет даже нулевым; Select("*") возьмёт всю структуру целиком, включая нули. Omit работает и со структурой, и с картой. Всё это — тот же белый список с форсом нулей, что и второй аргумент Where из седьмого урока, только применённый к SET.
Что уедет в SET?
Прочитайте код и выберите, что он выведет:
// Задача 3 «Ответить заказчику»: в базе done = false, priority = 2
var task TodoItem
db.First(&task, 3)
result := db.Model(&task).Updates(TodoItem{
Done: false,
Priority: 5,
Status: "done",
})
fmt.Println(result.RowsAffected, result.Error)Select и Omit: состав SET под контролем#
Оба метода в обновлении — про белый список колонок, и это зеркальное отражение Specify Struct search fields из седьмого урока: только вместо WHERE фильтруется SET. Select перечисляет, что ехать: перечисленное включается даже нулевым — единственный структурный способ записать ноль без карты. Select("*") берёт всю структуру как есть, с нулями, — способ «передать модель целиком». Omit вычёркивает колонку — тоже и со структурой, и с картой: так обновляют всё, кроме опасной колонки, скажем slug или version, случайная перезапись которых ломает данные. У приёма та же обратная сторона, что у условий: рестрикт. Забытая в Select колонка молча не поедет — запрос завершится успехом, а поле останется старым, и никакая ошибка об этом не скажет. Отсюда дисциплина: состав SET критичен — перечисляйте Select явно и смотрите SQL-след перед отправкой; пусть лучше лишнее уедет осознанно, чем нужное пропадёт молча.
Проверка: Select с нулевым полем
Что запишет в базу db.Model(&task).Select("Done").Updates(TodoItem{Done: false})?
RowsAffected: сколько строк реально тронуто#
У результата любого финишера есть result.RowsAffected, и для обновлений это главный ответ на вопрос «что произошло». UPDATE без совпадений — законная ситуация: задачу удалил другой пользователь, ключ остался от старого кеша, фильтр не совпал. GORM вернёт RowsAffected = 0 и Error = nil: не нашлось — не ошибка, в отличие от First с его ErrRecordNotFound из шестого урока. Отсюда две привычки. Первая: в API, который обещает клиенту конкретную запись, проверяйте «нашлась ли строка» — иначе пользователь получит успешный 200 OK при несуществующем id. Вторая: знать, что именно считает база. PostgreSQL возвращает все строки, затронутые UPDATE, даже если значения не изменились. MySQL по умолчанию считает только реально изменённые: SET priority = 2 на строке, где уже priority = 2, даст ноль. Драйвер go-sql-driver имеет параметр DSN clientFoundRows=true — он возвращает MySQL к поведению «найденных» строк. SQLite считает затронутые строки, как PostgreSQL: проверка запросом с неизменяющим SET это покажет за минуту. Для атомарных инкрементов и блокировок ниже RowsAffected == 0 станет точным признаком проигранной гонки.
Сколько строк обновит батч?
Прочитайте код и выберите, что он выведет:
// Рабочее состояние: задачи 2 и 4 в статусе todo
result := db.Model(&TodoItem{}).
Where("status = ?", "todo").
Update("status", "doing")
fmt.Println(result.RowsAffected, result.Error)UpdateColumn и UpdateColumns: SkipHooks из finisher_api.go#
В finisher_api.go рядом с Update живут два метода, отличающиеся одной строкой: tx.Statement.SkipHooks = true. UpdateColumn(column, value) — для одного поля, UpdateColumns(values) — для пачки, структура или карта. SkipHooks выключает сразу два слоя автоматики. Первый — хуки модели: BeforeSave и BeforeUpdate перед запросом, AfterSave и AfterUpdate после; служебная правка не должна дёргать бизнес-логику, которую вы повесили на модель. Второй — автоправка времени. У полей с меткой AutoUpdateTime (ваш UpdatedAt из схемы — time.Time поле размечается автоматически) каждый Update и Updates подменяют значение текущим временем: при структурном обновлении поле всегда пишется now, при обновлении картой — если ключа updated_at нет среди прочих; картой время можно и перезаписать явно, положив своё значение в ключ. UpdateColumn и UpdateColumns этого не делают: updated_at остаётся каким был. Смысл прозрачен: updated_at должен означать «пользователь тронул запись», а не «скрипт пересчитал счётчик». Учтите: защита от глобальных обновлений на них распространяется в полном объёме — WHERE нужен и здесь.
Update против UpdateColumn: SQL-след
GO// Update: колонка + автоправка updated_at, вокруг запроса - хуки
db.Model(&task).Update("priority", 2)
// UPDATE "todo_items" SET "priority"=2,"updated_at"='...' WHERE id = 3
// UpdateColumn: ровно то, что попросили - без времени и без хуков
db.Model(&task).UpdateColumn("priority", 2)
// UPDATE "todo_items" SET "priority"=2 WHERE id = 3
// UpdateColumns: то же для пачки, структура или карта
db.Model(&task).UpdateColumns(map[string]interface{}{"priority": 2})
// UPDATE "todo_items" SET "priority"=2 WHERE id = 3
// Выражения работают и здесь: декремент без гонки и без updated_at
db.Model(&task).Where("priority > ?", 0).
UpdateColumn("priority", gorm.Expr("priority - ?", 1))
// UPDATE "todo_items" SET "priority"=priority-1 WHERE id = 3 AND priority > 0Следы отличаются одним слагаемым — и этим всё сказано: UpdateColumn правит то, что вы просили, и ничего больше. Классическое применение — счётчик просмотров или остаток на складе: тысячи обновлений в минуту, у каждого своя семантика времени, и updated_at от них шалеть не должен. Заметьте: WHERE требуется и здесь — checkMissingWhereConditions охраняет всю четвёрку методов.
Обновление без условий GORM не выполнит: db.Model(&TodoItem{}).Update("status", "done") вернёт ErrMissingWhereClause — в errors.go его текст ровно такой: «WHERE conditions required». Проверка живёт в callbacks/helper.go (checkMissingWhereConditions) и выполняется до отправки запроса: Error не nil, RowsAffected = 0, база не тронута — случайно закрыть все задачи одной строкой не получится. Это защита от самой дорогой опечатки CRUD: пропущенный Where — и пол-таблицы с новым значением. Легальных выхода три, и все осознанные: тривиальное условие Where("1 = 1"), сырой SQL через db.Exec (строка Exec не экранируется — ввод пользователя только аргументами, гигиена седьмого урока) или режим AllowGlobalUpdate на сессию. Нюанс на полях: если модель несёт gorm.DeletedAt, к каждому UPDATE дописывается deleted_at IS NULL — но сам по себе он условием не считается, детали девятого урока.
Три осознанных выхода из запрета
GO// Запрещено: условий нет
result := db.Model(&TodoItem{}).Update("status", "done")
fmt.Println(result.Error, result.RowsAffected)
// WHERE conditions required 0 - запрос в базу даже не ушёл
// Выход 1: тривиальное условие - подпись под массовой операцией
db.Model(&TodoItem{}).Where("1 = 1").Update("status", "done")
// UPDATE "todo_items" SET "status"='done',"updated_at"='...' WHERE 1=1
// Выход 2: сырой SQL - для скриптов и сложных выражений
db.Exec("UPDATE todo_items SET status = 'done'")
// UPDATE todo_items SET status = 'done'
// Выход 3: AllowGlobalUpdate на сессию
db.Session(&gorm.Session{AllowGlobalUpdate: true}).
Model(&TodoItem{}).Update("status", "done")
// UPDATE "todo_items" SET "status"='done',"updated_at"='...'Все три пути легальны, но читаются по-разному. Where("1 = 1") — честная подпись под массовой операцией: её видно и в коде, и в следе. db.Exec — территория сырого SQL: аргументы подставляйте плейсхолдерами, ввод пользователя в строку не вклеивайте; затронутые строки считаются через result.RowsAffected и здесь. AllowGlobalUpdate можно задать и в конфиге подключения, и на конкретную сессию — второй способ лучше: опасный участок локализован одной цепочкой, и поиск по кодовой базе его находит.
gorm.Expr: атомарные инкременты без гонки#
Классический баг конкурентности: два воркера поднимают приоритет одной задачи. Оба читают priority = 2, оба прибавляют единицу в Go, оба пишут 3 — одно повышение потеряно. Схема «прочитал — посчитал — записал» ломается всегда, когда между чтением и записью может вклиниться кто-то ещё, а может он почти всегда. gorm.Expr из gorm.go разрубает узел: func Expr(expr string, args ...interface{}) clause.Expr — выражение передаётся базе как значение, и SET priority = priority + 1 вычисляет СУБД в момент исполнения над актуальной строкой. Один запрос, никакого чтения, никакого окна для гонки. Аргументы Expr едут плейсхолдерами и экранируются драйвером — то же правило седьмого урока: переменные живут в аргументах, а не в строке выражения. Форма работает везде, где ждут значение: в Update, в карте Updates, в UpdateColumn и UpdateColumns. Внутри выражения — любой SQL, который поймёт ваш диалект: арифметика, функции, CASE. Переносимость выражения — забота ваша: GORM текст выражения не разбирает и не проверяет.
Инкременты и декременты
GO// Гонка: так делать не надо
var task TodoItem
db.First(&task, 3)
db.Model(&task).Update("priority", task.Priority+1)
// между First и Update сосед успел поднять приоритет - его правка затёрта
// Атомарно: вычисление уезжает в СУБД
db.Model(&task).Update("priority", gorm.Expr("priority + ?", 1))
// UPDATE "todo_items" SET "priority"=priority+1,"updated_at"='...' WHERE id = 3
// То же картой Updates - выражение легально среди обычных значений
db.Model(&task).Updates(map[string]interface{}{
"status": "doing",
"priority": gorm.Expr("priority + ?", 1),
})
// UPDATE ... SET "priority"=priority+1,"status"='doing',"updated_at"='...' WHERE id = 3
// Батч: поднять приоритет всем открытым задачам одним запросом
db.Model(&TodoItem{}).Where("status IN ?", []string{"todo", "doing"}).
Update("priority", gorm.Expr("priority + ?", 1))
// UPDATE ... SET "priority"=priority+1 WHERE status IN ('todo','doing')След атомарного запроса стоит рассмотреть медленно: справа от знака равенства не число, а ссылка на колонку — формула, которую база вычислит над текущей строкой. Выражение можно смешивать с обычными значениями в одной карте: каждая пара независимо решает, значение это или формула. Батч-вариант — рабочая замена циклу из N запросов: одна строка SQL, один поход в базу.
Конкурентные правки: оптимистичная блокировка через версию#
Инкремент закрыл арифметику, но правки бывают сложнее: два менеджера редактируют одну задачу в разных окнах, оба жмут «Сохранить» — чья версия переживёт, та и останется, вторая сгинет молча. Это потерянное обновление, и у него два канонических ответа. Первый — оптимистичная блокировка: к модели добавляется счётчик версий, читаем его вместе с данными, а при записи ставим двойное условие — ключ и та самая версия — и наращиваем счётчик атомарно через gorm.Expr. Кто-то успел раньше? Его запрос уже поднял версию, наше Where("version = ?") промахнулось, RowsAffected = 0 — и мы честно узнаём о конфликте вместо тихой порчи данных. Никаких блокировок СУБД не держит, отсюда имя «оптимистичная»: рассчитываем, что конфликтов мало, и проверяем постфактум. Поле добавить легко: Version int в структуру задачи — AutoMigrate из четвёртого урока добавит колонку сам. Схема хорошо ложится на веб: перечитать и повторить — штатный путь, а не авария; данные читаются без транзакций; конфликты редки, потому что разные клиенты правят разные записи. Держите её как дефолт, а пессимистичную из следующего раздела оставьте горячим строкам.
Оптимистичная блокировка на версии
GO// В модель добавлено поле Version int - счётчик конкурентных правок
// (AutoMigrate из четвёртого урока добавит колонку сам)
func closeTask(db *gorm.DB, id uint) error {
var task TodoItem
// Читаем задачу вместе с её текущей версией
if err := db.First(&task, id).Error; err != nil {
return err
}
// Записываем только если версия не изменилась с момента чтения
result := db.Model(&TodoItem{}).
Where("id = ? AND version = ?", task.ID, task.Version).
Updates(map[string]interface{}{
"status": "done",
"version": gorm.Expr("version + ?", 1), // наращиваем атомарно
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
// Кто-то сохранил раньше: версия уже другая
return errors.New("конфликт версий: перечитайте задачу и повторите")
}
return nil
}Весь паттерн — один UPDATE: и проверка версии, и запись нового значения, и её инкремент уезжают вместе, между ними никто не вклинится. RowsAffected == 0 здесь имеет ровно один смысл: задачу правили параллельно. Промышленная версия паттерна зацикливает перечитать-повторить с лимитом попыток; для API ответ «данные изменились, обновите форму» — тоже легальный исход.
Пессимистичная блокировка: SELECT FOR UPDATE#
Второй ответ — пессимистичная блокировка: заблокировать строку до правки, чтобы конкурент даже не пытался. Рецепт: транзакция, внутри — чтение с клозом блокировки tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&task, 3), и SELECT уезжает в базу с хвостом FOR UPDATE: строка заблокирована до COMMIT или ROLLBACK, конкурентная транзакция с таким же FOR UPDATE повиснет на своём чтении и дождётся нашей. Клоз описан в clause/locking.go: поле Strength принимает константы LockingStrengthUpdate ("UPDATE") и LockingStrengthShare ("SHARE" — FOR SHARE, блокировка от изменений, но не от чтений), поле Options — LockingOptionsNoWait ("NOWAIT") и LockingOptionsSkipLocked ("SKIP LOCKED"): не ждать занятую строку, а сразу ошибиться или молча пропустить. Заметьте, что FOR UPDATE едет в SELECT: клоз FOR входит в сборку запроса на чтение, а правка потом делается обычным Update — блокирует чтение, а не запись. Транзакция не опция, а обязательное условие: без неё SELECT живёт в автокоммите и замок рассыпается сразу после чтения. Выбор между схемами прагматичен: короткие правки и редкие конфликты — оптимистичная; горячая строка, которую щупают все воркеры одновременно, — пессимистичная, чтобы конкуренция выстроилась в очередь, а не в бесконечные повторы.
FOR UPDATE внутри транзакции
GOerr := db.Transaction(func(tx *gorm.DB) error {
var task TodoItem
// SELECT * FROM "todo_items" WHERE id = 3 ORDER BY ... LIMIT 1 FOR UPDATE
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&task, 3).Error; err != nil {
return err
}
// Строка заблокирована до конца транзакции: конкурентный FOR UPDATE подождёт
if task.Status == "done" {
return errors.New("задача уже закрыта")
}
return tx.Model(&task).Update("status", "done")
})
if err != nil {
log.Println("не получилось:", err)
}Два правила кода. Первое: внутри колбэка работаем с tx, а не с db — только соединение транзакции держит замок; уйдёте на db — заблокируете строку в одном соединении, а правите в другом. Второе: FOR UPDATE в следе сидит в самом SELECT — правка отдельным Update уже ни о чём не беспокоится. Нужен сценарий «не ждать» — добавьте Options: "SKIP LOCKED" в тот же клоз: занятая строка молча выпадет из выборки, так строят очереди воркеров.
Шпаргалка: методы обновления#
| Вызов | Что в SET | updated_at и хуки | Нюанс |
|---|---|---|---|
| Model(&task).Update("col", v) | одна колонка | авто, хуки работают | WHERE из ключа модели |
| Model(...).Where(...).Update(...) | одна колонка, батч | авто, хуки работают | без условия — ErrMissingWhereClause |
| Updates(struct) | ненулевые поля | авто, хуки работают | zero values молча выпадают |
| Updates(map) | все ключи, с нулями | авто, если updated_at нет в карте | колонки в SET по алфавиту |
| Select("Done").Updates(...) | перечисленное, даже нули | авто | рестрикт: остальное не поедет |
| UpdateColumn / UpdateColumns | одна колонка / пачка | не трогаются, SkipHooks | WHERE всё равно обязателен |
| Update("col", gorm.Expr(...)) | SQL-выражение | авто | атомарно, без гонки читать-писать |
| Clauses(Locking{...}).First(...) | SELECT ... FOR UPDATE | — | только внутри транзакции |
Заполните пропуски: атомарность и проверка результата
Восстановите атомарный инкремент, карту с честным нулём и проверку затронутых строк.
Соберите путь UPDATE по шагам
Расставьте шаги, которые проходят данные при вызове Model(&task).Update("status", "done").
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: обновление записей
Что дальше по курсу#
CRUD почти собран: C из пятого урока, R из шестого и седьмого, U из этого — осталась D, и она интереснее, чем кажется. Девятый урок — Удаление и мягкое удаление: Delete по ключу и по условию, поле gorm.DeletedAt, из-за которого «удалённые» строки продолжают жить в таблице, Unscoped для настоящей чистки и даже восстановление; там же станет понятен deleted_at IS NULL, мелькавший в этом уроке одной фразой. Если нужно освежить формы условий, вернитесь к седьмому уроку про Where; чтение, First и ErrRecordNotFound — в шестом уроке; полный план из 25 уроков — на странице курса. Домашка ниже собирает все финишеры обновления и обе блокировки в один скрипт.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Update("col", v) обновляет одно поле: WHERE приходит из первичного ключа Model(&task) или из явного Where; без условия GORM вернёт ErrMissingWhereClause и в базу не пойдёт.
- Updates со структурой берёт только ненулевые поля — Done: false молча выпадет; карта включает нули честно, а её ключи в SET идут по алфавиту.
- Select("Done") в обновлении форсирует перечисленное поле даже нулевое, Omit вычёркивает колонку — белый список для SET, зеркало Specify Struct search fields.
- UpdateColumn и UpdateColumns ставят SkipHooks: ни BeforeSave/BeforeUpdate, ни AfterSave/AfterUpdate, ни автоправки updated_at — для счётчиков, аудита и служебных правок.
- RowsAffected — сколько строк реально затронуто; ноль строк — не ошибка, а сигнал «не нашлось» или «проиграли гонку»; MySQL по умолчанию считает только изменённые, PostgreSQL — все затронутые.
- gorm.Expr отдаёт базе выражение: Update("priority", gorm.Expr("priority + ?", 1)) атомарен — SET вычисляет СУБД над актуальной строкой, окна для гонки нет.
- Конкурентные правки: оптимистичная блокировка — поле Version, Where("id = ? AND version = ?") и проверка RowsAffected == 0; пессимистичная — транзакция и Clauses(clause.Locking{Strength: "UPDATE"}) на SELECT, то есть FOR UPDATE до коммита.
Домашнее задание
Соберите локальный скрипт поверх таблицы из пятого урока. Закройте задачу 3 через Model(&task).Update("status", "done") и найдите в следе WHERE id = 3 и подмену updated_at. Попробуйте закрыть всё одним Update без условий — прочитайте ErrMissingWhereClause, затем обойдите его тремя способами: Where("1 = 1"), db.Exec, Session с AllowGlobalUpdate — и сравните RowsAffected каждого. Обновите задачу 2 структурой с Done: false и убедитесь, что поле промолчало; почините картой и Select("Done"). Поднимите приоритет всем открытым задачам через gorm.Expr("priority + ?", 1) одним запросом. Смоделируйте конфликт версий: два чтения задачи и две правки подряд — вторая должна получить RowsAffected == 0. Финал: повторите ту же правку через транзакцию с clause.Locking{Strength: "UPDATE"} и найдите FOR UPDATE в SQL-следе.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему Updates со структурой не обновляет поле, которому присвоили ноль?
Структурное обновление собирается из ненулевых полей: false, 0 и пустая строка считаются «не заполнено» — то же isZero-правило, что и в структурных условиях седьмого урока. Нужен честный ноль — Updates с map[string]interface{}, где карта не отличает «пусто» от «не заполнял», или Select("Поле") с перечислением нужной колонки.
Как обновить все строки таблицы, если GORM требует WHERE?
Три легальных пути: тривиальное условие Where("1 = 1"), сырой SQL через db.Exec (ввод пользователя — только аргументами, строка Exec не экранируется) или Session(&gorm.Session{AllowGlobalUpdate: true}) для осознанного глобального обновления. Без всего этого Update вернёт ErrMissingWhereClause с текстом «WHERE conditions required» и не отправит запрос в базу.
Когда брать UpdateColumn вместо Update?
Когда правка служебная: счётчики, аудит, синхронизация данных — там, где не нужны хуки BeforeSave/BeforeUpdate/AfterSave/AfterUpdate и не должно двигаться updated_at. Внутри UpdateColumn стоит SkipHooks. Условие всё равно обязательно: защита от глобальных обновлений через checkMissingWhereConditions действует и на него.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
sqlc: типобезопасный код из SQL-запросов
Курс sqlc: настройка, запросы, параметры, NULL-типы, транзакции, джойны, пагинация, CI и сравнение с ORM. 25 уроков с практикой.
- Настраивать sqlc.yaml для Postgres/MySQL/SQLite
- Писать аннотированные запросы с типизированными параметрами
- Работать с NULL, массивами и JSON-колонками
- Организовывать транзакции через WithTx