Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 8 из 25 ~18 мин +50 XP

Обновление записей — Update, Updates, UpdateColumn, gorm.Expr и блокировки

Чтение и фильтрация из двух предыдущих уроков закрыли половину CRUD — настала очередь обновлений. gorm update — это три метода разного калибра: Update для одного поля, Updates для пачки колонок, UpdateColumn для служебных правок без хуков, плюс gorm.Expr — способ отдать базе SQL-выражение вместо значения. Обновление — самая коварная операция из четырёх: INSERT при ошибке ругается сразу, а UPDATE с широким условием молча перепишет половину таблицы, и узнают об этом пользователи. Поэтому в уроке не только синтаксис, но и защита: ErrMissingWhereClause против глобальных правок, RowsAffected как ответ на вопрос «а строка-то нашлась?», и две схемы борьбы за одну строку — оптимистичная блокировка с версией и пессимистичная SELECT FOR UPDATE. Рабочие данные — те же пять задач в todo_items в состоянии из седьмого урока: «Купить хлеб» и «Обновить бюджет» закрыты, «Полить цветы» и «Заплатить за интернет» ждут, «Ответить заказчику» в работе, приоритеты 0, 0, 2, 3, 1. Одна честная оговорка о формате: СУБД в браузерной песочнице не живёт, поэтому runner-ов в курсе нет — вас проверяют quiz, predict, fill, order и карточки, а примеры рассчитаны на локальный Go с PostgreSQL или SQLite. Каждый пример снабжён SQL-следом — его печатает логгер из второго урока.

Содержание урока · 13 разделов

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 охраняет всю четвёрку методов.

Три осознанных выхода из запрета

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 внутри транзакции

GO
err := 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" в тот же клоз: занятая строка молча выпадет из выборки, так строят очереди воркеров.

Шпаргалка: методы обновления#

ВызовЧто в SETupdated_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одна колонка / пачкане трогаются, SkipHooksWHERE всё равно обязателен
Update("col", gorm.Expr(...))SQL-выражениеавтоатомарно, без гонки читать-писать
Clauses(Locking{...}).First(...)SELECT ... FOR UPDATE—только внутри транзакции

Заполните пропуски: атомарность и проверка результата

Восстановите атомарный инкремент, карту с честным нулём и проверку затронутых строк.

// 1) Поднять приоритет задачам в работе - атомарно db.Model(&TodoItem{}).Where("status = ?", "doing"). Update("priority", gorm.("priority + ?", )) // 2) Переписать задачу картой: нулевые значения едут честно db.Model(&task).Updates(map[string]interface{}{"": false, "status": "todo"}) // 3) Проверить, что строка нашлась и версия совпала if result. == 0 { log.Println("задача не найдена или версия не совпала") }

Соберите путь UPDATE по шагам

Расставьте шаги, которые проходят данные при вызове Model(&task).Update("status", "done").

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: обновление записей

Термин

Update("col", v)

нажмите, чтобы перевернуть
Определение

Одно поле: Model(&task) даёт WHERE id из ключа, Where() — своё условие. Хуки и автоправка updated_at работают. Без условия — ErrMissingWhereClause («WHERE conditions required»), запрос в базу не уходит.

1 / 6

Что дальше по курсу#

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-следе.

Личные заметкиПусто
Хранятся только в вашем браузере (GORM · Обновление записей — Update, Updates, UpdateColumn, gorm.Expr и блокировки)

Вопросы и ответы по уроку

Почему 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 действует и на него.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

sqlc25 уроков~12 чпрактика25 уроков уже доступны

sqlc: типобезопасный код из SQL-запросов

Курс sqlc: настройка, запросы, параметры, NULL-типы, транзакции, джойны, пагинация, CI и сравнение с ORM. 25 уроков с практикой.

  • Настраивать sqlc.yaml для Postgres/MySQL/SQLite
  • Писать аннотированные запросы с типизированными параметрами
  • Работать с NULL, массивами и JSON-колонками
  • Организовывать транзакции через WithTx