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

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

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

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

Создание сущностей

Семь уроков мы готовили сцену: схемы User и Group, поля с валидаторами, рёбра, карта каталога ent/. Сегодня — первое настоящее действие над базой: создание ent-сущностей. Форма вызова непривычна для пришедших из других ORM: client.User.Create() не принимает ни контекста, ни готовой структуры — из него выходит билдер, а исполнение начинается только в Save(ctx). Тема урока — ent create целиком: сеттер-стиль, три контракта ошибки Save, SaveX и Exec, SetNillable-сеттеры для значений-указателей, связи при создании одной цепочкой, пакетная вставка CreateBulk и upsert через OnConflict для сценария «нет — вставь, есть — обнови». Финал — ошибки во всех проявлениях: валидация до базы, дубли уникального индекса и отменённый контекст.

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

Мы вызывали client.User.Create() начиная с третьего урока — в квизах, в predict-задачах, в цепочках с AddUsers из шестого урока. Но делали это вслепую: билдер был чёрным ящиком, который почему-то исполняется методом Save. Сегодня вскрываем ящик и проходим ent create от формы вызова до классификации ошибок. Сначала форма: почему Create() не принимает ни контекста, ни структуры и что именно он возвращает. Затем исполнение: Save, SaveX, Exec и ExecX — четыре метода, два контракта ошибки. Потом арсенал: SetNillable для nil-значений из API-слоя, связи при создании, пакетная вставка CreateBulk одним INSERT и upsert через OnConflict — включённый в седьмом уроке флаг sql/upsert наконец находит своё применение. И в конце — бухгалтерия ошибок: где ent проверяет поля, как он классифицирует дубли уникального индекса и что происходит с отменённым контекстом. Создание ent после этого урока перестанет быть вызовом по памяти и станет осознанной операцией.

ent create: билдер вместо готовой структуры#

Главная привычка, которую придётся поменять: в ent нет формы Create(ctx, User), принимающей готовую структуру. client.User.Create() вызывается без единого аргумента и возвращает UserCreate — билдер, внутри которого живёт UserMutation в режиме OpCreate. Дальше вы метод за методом наполняете мутацию: SetName записывает поле, AddGroupIDs добавляет ребро. Контекст появляется только на исполнении — Save(ctx). Это не случайность, а разделение ролей: сборка и исполнение — разные шаги. Билдер можно собрать один раз и исполнить в двух разных контекстах, отдать в функцию, которая дополнит его своими полями, повесить на него хук — мутация и есть то, на что подписываются хуки семнадцатого урока. А привычку «создать из структуры» ent предлагает заменить тем, что вы перекладываете поля сеттерами: компилятор проверит каждое, и ни одно не потеряется молча. Ещё одна особенность станет важна ниже: у билдера нет метода ID(ctx) — он появится только после OnConflict(), потому что «вернуть id вставленного» осмысленно и для обновлённой строки, а это уже другой тип билдера.

Сеттер-стиль: Create().SetName(...).Save(ctx)

GO
// client.User.Create() не принимает ни ctx, ни структуры —
// он возвращает билдер, исполнение начинается в Save(ctx):
anna, err := client.User.
        Create().                    // *UserCreate с мутацией OpCreate
        SetName("anna").             // обязательное поле из схемы
        SetEmail("anna@entgo.io").   // email — уникальный
        SetAge(28).                  // Positive-валидатор из третьего урока
        SetWebsite("https://anna.dev"). // Optional-поле из четвёртого
        Save(ctx)                    // INSERT и возврат (*ent.User, error)

Порядок сеттеров свободный — вы наполняете мутацию, а не вызовы. Save возвращает пару: сущность с присвоенным ID и ошибку. Обязательное поле name и Unique-поле email взяты из схемы второго урока; забудете SetName — ошибка придёт ещё до базы (ниже покажем, прежде каких этапов). Вся цепочка — обычный Go: опечатка в имени сеттера — ошибка компиляции, а не рантайма. Это тот заслон, ради которого ent и генерирует билдеры вместо строковых колонок.

Save, SaveX, Exec, ExecX: два контракта ошибки#

У каждого create-билдера четыре метода исполнения, и различаются они двумя осями: вернуть сущность или нет, вернуть ошибку или паниковать. Save(ctx) — основной: (*User, error), полный контроль в руках вызывающего. SaveX(ctx) — то же самое, но вместо ошибки паника: документация описывает его одной фразой «SaveX calls Save and panics if Save returns an error». Exec(ctx) — для случаев, когда сущность не нужна: метод возвращает только error, это удобный финал для цепочек-побочек вроде записи лога или факта «пользователь кликнул». ExecX(ctx) — Exec с паникой. Правило выбора простое: где ошибку обрабатывают — там Save и Exec (HTTP-хендлеры, воркеры), где ошибке нечего делать — там SaveX (инициализация приложения, сидинг демо-данных, тесты, CLI-утилиты). Паника из SaveX в HTTP-хендлере превратится в 500 через recovery-механизм фреймворка — переживёт пользователь, но не ваша логика повторов. Поэтому в прикладном коде каноничен Save, а SaveX — рабочая лошадка кода запуска и тестов.

SetNillable: сеттеры, которые не боятся nil#

API-слои любят указатели: поле формы пришло как string, и непонятно, то ли это «пусто», то ли значение. Для таких случаев генератор выпускает SetNillable-сеттеры: SetNillableWebsite(p string) устанавливает поле, только если p не nil, — в документации это звучит как «Avoid nil checks»: одну if-проверку на каждое поле срезает сам сеттер. Передали nil — сеттер молча пропустит, поле останется незаданным, и для Optional-колонки это законный исход: Save пройдёт, в базе будет NULL. SetNillable генерируется у полей с Optional и у полей с Default — роль SetNillableRole(string) существует, хотя сама роль обязательна: nil лишь значит «возьми дефолт». Здесь же честная тонкость, дополняющая таблицу четвёртого урока: у Nillable-полей отдельный SetNillable не генерируется вовсе. Для bio, объявленного Optional().Nillable(), сеттер SetBio уже принимает string — null-safe-версия была бы полным дублем, и в шаблоне генератора этот случай исключён комментарием про то, что nillable-сеттеры для nillable-типов не нужны. Итого: website — пара SetWebsite(string) и SetNillableWebsite(string); bio — только SetBio(string) плюс ClearBio.

Проверка: nil в SetNillable

website объявлен в схеме как Optional (без Nillable). Что произойдёт при вызове client.User.Create().SetName("vera").SetEmail("vera@entgo.io").SetNillableWebsite(p).Save(ctx), если p — nil *string?

Связи при создании: AddGroups одной цепочкой

GO
// группы g1 и g2 уже созданы и лежат в переменных:
anna, err := client.User.
        Create().
        SetName("anna").
        SetEmail("anna@entgo.io").
        AddGroups(g1, g2).   // M2M: две строки в group_users
        Save(ctx)

// вариант по идентификаторам — без структур на руках:
boris, err := client.User.
        Create().
        SetName("boris").
        SetEmail("boris@entgo.io").
        AddGroupIDs(g1.ID, 42).
        Save(ctx)

AddGroups и AddGroupIDs — та же пара «по сущностям и по идентификаторам», что у AddUsers в шестом уроке, только с обратной стороны ребра groups: обратная половина тоже получает свои Add-мутаторы в user_create.go. Под капотом sqlgraph это несколько операций: INSERT в users, затем вставки в join-таблицу group_users. Важная деталь исходников: как только в операции появляются «внешние» рёбра, ent сам открывает транзакцию (внутри это mayTx), исполняет вставку узла и связи единым блоком и откатывает всё при любой ошибке. Вручную оборачивать такой create в Tx не нужно — транзакция уже есть.

Забытое поле: какая ошибка вернётся

Прочитайте код и выберите, что он выведет:

// в схеме: age — обязательное поле с валидатором,
// email — Unique, name — обязательное

u, err := client.User.
        Create().
        SetName("boris").
        SetEmail("boris@entgo.io").
        // SetAge забыли
        Save(ctx)

fmt.Println(u, err)

CreateBulk: пакетная вставка одним INSERT#

Десять пользователей по одному Save — это десять INSERT и десять кругов до базы. CreateBulk собирает их в один. Точная сигнатура из сгенерированного клиента: client.User.CreateBulk(builders ...UserCreate) UserCreateBulk — метод принимает уже собранные create-билдеры, а его собственный Save возвращает ([]*User, error): сущности в том же порядке, что и билдеры. Внутри это sqlgraph.BatchCreate: один INSERT с несколькими кортежами VALUES. Следствия читаются прямо из этой механики. Если билдеры задали разные наборы полей, ent выровняет колонки, заполнив пропуски NULL-ами — INSERT требует одинаковой арки. Если ID задан хоть у одного билдера, он обязан быть у всех — иначе ошибка «inconsistent id values for batch insert»: частично заданные идентификаторы ломают расчёт id на MySQL и SQLite. Ошибка в любой строке пакета — ошибка всей операции: стейтмент один, «полу-вставки» не существует. А если у билдеров есть рёбра, ent снова откроет транзакцию сам: узлы и join-строки покатятся вместе. Для типового сценария «вставить всех из среза» есть второй вход — MapCreateBulk, он же и есть способ не собирать билдеры руками.

CreateBulk и MapCreateBulk: два входа в пакет

GO
// вход 1: билдеры собраны заранее, каждый по-своему:
users, err := client.User.CreateBulk(
        client.User.Create().SetName("anna").SetEmail("anna@entgo.io"),
        client.User.Create().SetName("boris").SetEmail("boris@entgo.io"),
).Save(ctx) // []*ent.User в порядке билдеров

// вход 2: MapCreateBulk строит билдеры из вашего среза сам:
names := []string{"clara", "dave", "vera"}
users, err := client.User.MapCreateBulk(names, func(c *ent.UserCreate, i int) {
        c.SetName(names[i]).SetEmail(names[i] + "@entgo.io")
}).Save(ctx)

MapCreateBulk(slice any, setFunc func(*ent.UserCreate, int)) пробегает срез с индексом и передаёт вам свежий билдер на каждый элемент — вместо ручного списка получается один колбэк. Аргумент slice нетипизирован (any): передадите не срез — ошибка уже на Save с текстом про «wrong type, need slice». Оба входа заканчиваются одинаково: один INSERT, все-или-ничего. Имена в пакетах продублировать нельзя: уникальный email в двух строках одного INSERT уронит весь пакет с ошибкой констрейнта — как её ловить, увидим ниже.

Проверка: атомарность пакета

Пять билдеров ушли в CreateBulk().Save(ctx). У третьего email уже занят в базе. Что лежит в базе и в переменных после вызова?

OnConflict: upsert «есть — обнови»#

Регистрация по email — сценарий «нет пользователя — вставь, есть — обнови». В SQL это INSERT ... ON CONFLICT (PostgreSQL, SQLite) или INSERT ... ON DUPLICATE KEY UPDATE (MySQL), в ent — методы из флага sql/upsert, который мы включали и изучали в седьмом уроке. Метод OnConflict(opts ...sql.ConflictOption) вызывается на create-билдере, но после него цепочка меняет тип: OnConflict() возвращает UserUpsertOne — отдельного upsert-билдера, и обычные SetX на нём уже недоступны. Повторный вызов OnConflict на том же билдере даёт ошибку «ent: OnConflict was set for builder» — флаг ставится один раз. Методы нового билдера: UpdateNewValues() — при конфликте обновить изменяемые поля значениями из вставки (Immutable-поля вроде created_at и заданный вручную ID генератор честно исключает); Ignore() — «каждой колонке её же значение», то есть фактически ничего не менять; DoNothing() — честный DO NOTHING, доступный только SQLite и PostgreSQL: в MySQL такого нет, ent подменяет его Ignore-вариантом; Update(func(u ent.UserUpsert)) — полный контроль над SET-частью. Пустой OnConflict() без цели конфликта MySQL поймёт (ON DUPLICATE KEY цели не имеет), а PostgreSQL потребует указать цель — об этом отдельный predict ниже. Исполнение — Exec(ctx) или ID(ctx): второй вернёт идентификатор вставленной или обновлённой строки.

OnConflict: три рабочие формы

GO
// 1. есть — обнови новыми значениями из вставки:
err := client.User.
        Create().
        SetEmail("anna@entgo.io").
        SetName("anna-updated").
        OnConflictColumns(user.FieldEmail). // цель конфликта
        UpdateNewValues().                  // mutable-поля из вставки
        Exec(ctx)

// 2. есть — обнови, но одно поле переопредели:
err := client.User.
        Create().
        SetEmail("boris@entgo.io").
        SetName("boris").
        OnConflictColumns(user.FieldEmail).
        UpdateNewValues().
        Update(func(u *ent.UserUpsert) {
                u.SetName("boris-returned") // свой SET поверх
        }).
        Exec(ctx)

// 3. вставь или обнови — и верни id строки:
id, err := client.User.
        Create().
        SetEmail("clara@entgo.io").
        SetName("clara").
        OnConflictColumns(user.FieldEmail).
        UpdateNewValues().
        ID(ctx) // id вставленной ИЛИ обновлённой

user.FieldEmail — константа "email" из пакета ent/user. UpdateNewValues внутри — это sql.ResolveWithNewValues() плюс исключения для Immutable-полей: created_at не перепишется, хотя значение в вставке есть. Update(func(u *ent.UserUpsert)) даёт весь словарь: SetX — своё значение, UpdateX — взять значение из вставки (внутри SetExcluded), AddX — прибавить к текущему, ClearX — в NULL. Форма 3 — самая частая в REST-обработчиках: один вызов вместо «сначала SELECT, потом INSERT или UPDATE». За кулисами ID(ctx) на PostgreSQL отработает через RETURNING, а на MySQL ent добавит в ON DUPLICATE KEY UPDATE выражение LAST_INSERT_ID(...) — иначе после конфликта MySQL вернул бы id 0. Для нечисловых (строковых, UUID) ID на MySQL ID(ctx) не поддержан — только Exec.

PostgreSQL без цели конфликта

Прочитайте код и выберите, что он выведет:

// диалект postgres, флаг sql/upsert включён.
// email "anna@entgo.io" уже есть в базе.

err := client.User.
        Create().
        SetEmail("anna@entgo.io").
        SetName("anna-the-second").
        OnConflict().          // цель конфликта не указана
        UpdateNewValues().
        Exec(ctx)

Дубликат: как ent отдаёт ошибку уникальности#

Email в схеме Unique — второй пользователь с тем же адресом упрётся в уникальный индекс, и текст ошибки придёт от драйвера: MySQL скажет «Error 1062: Duplicate entry 'anna@entgo.io' for key 'users.email'», PostgreSQL — «duplicate key value violates unique constraint "users_email_key"», SQLite — «UNIQUE constraint failed: users.email». ent классифицирует такие ошибки в пакете dialect/sql/sqlgraph: функция IsUniqueConstraintError ищет в тексте ошибки маркеры всех трёх диалектов — именно те три строки, что выше. Параллельно есть более широкий IsForeignKeyConstraintError с маркерами нарушений внешних ключей. Дальше работает сгенерированный код: любой constraint-сбой оборачивается в тип ent.ConstraintError — его Error() даёт «ent: constraint failed: » плюс исходный текст, а Unwrap отдаёт оригинал, так что цепочка errors.Is и errors.As сохраняется целиком. Итого два уровня проверки на вашей стороне: ent.IsConstraintError(err) через errors.As — «это нарушение какого-либо констрейнта», и sqlgraph.IsUniqueConstraintError(err) — «это именно дубль». Честная деталь механизма: классификация строковая, по тексту драйвера, — поэтому ent и матчит только свои три маркера на диалект, а не пытается парсить всё подряд.

Ловим дубль email: два уровня проверки

GO
import (
        "blog/ent"
        "entgo.io/ent/dialect/sql/sqlgraph"
)

_, err := client.User.
        Create().
        SetName("anna").
        SetEmail("anna@entgo.io"). // адрес уже занят
        Save(ctx)

switch {
case err == nil:
        // вставка прошла
case ent.IsConstraintError(err): // errors.As: обёртка ent.ConstraintError
        if sqlgraph.IsUniqueConstraintError(err) { // именно дубль индекса
                // вернём форме «email занят», а не 500
        } else {
                // другой констрейнт: например, внешний ключ
        }
default:
        // остальное: соединение, диалект, контекст
}

ent.ConstraintError — единственная обёртка, которую ent добавляет к ошибкам создания: сообщение начинается с «ent: constraint failed: », а Unwrap отдаёт текст драйвера целиком — в лог попадёт и имя индекса, и значение. sqlgraph.IsUniqueConstraintError не строит цепочку через errors.As — он проверяет текст ошибки, и обёртка ему не мешает: исходный текст внутри. Функциональные обработчики ошибок валидации уже видели: IsValidationError из семейства тех же сгенерированных хелперов — порядок проверок «валидация, потом констрейнты» отражает порядок этапов Save.

Проверка: классификация ошибок

Создание поста упало: author_id ссылается на удалённого пользователя, драйвер вернул нарушение внешнего ключа. Что вернут ent.IsConstraintError(err) и sqlgraph.IsUniqueConstraintError(err)?

Отменённый контекст: что вернёт Save#

Контекст в ent — сквозной: Save(ctx) передаёт его драйверу, и отмена добирается до уровня database/sql. Клиент закрыл вкладку, дедлайн истёк — драйвер вернёт context.Canceled или context.DeadlineExceeded, и ent их не заворачивает и не переписывает: обычная проверка errors.Is(err, context.Canceled) работает без сюрпризов. Полезно знать порядок этапов, чтобы понимать, что успеет произойти до отмены. Сборка дефолтов, хуки и check() с валидацией — чистый Go, контекст им не нужен: невалидный билдер вернёт ValidationError даже на уже отменённом контексте, потому что до базы дело просто не дойдёт. И наоборот: идеально собранная вставка на отменённом контексте упадёт на драйвере, ни одна строка не будет записана. С пакетами та же логика плюс атомарность: один INSERT не умеет частично исполниться, а пакет с рёбрами накрывает всё авто-транзакцией — отмена на середине откатит и узлы, и join-строки. Поэтому повтор на свежем контексте — безопасная стратегия: ent не оставляет обломков между вызовами.

Арсенал создания: метод, сценарий, возврат#

ВызовСценарийВозврат
client.User.Create().SetX(...).Save(ctx)одна сущность, ошибку обрабатываем(*ent.User, error)
client.User.Create().SetX(...).SaveX(ctx)одна сущность, ошибка = паника (запуск, тесты)*ent.User
client.User.Create().SetX(...).Exec(ctx)факт вставки, сущность не нужнаerror
client.User.CreateBulk(b1, b2).Save(ctx)пакет собранных билдеров одним INSERT([]*ent.User, error)
client.User.MapCreateBulk(slice, fn).Save(ctx)пакет из среза: билдеры строит ent([]*ent.User, error)
...OnConflictColumns(...).UpdateNewValues().ID(ctx)upsert: вставь или обнови и верни id(int, error)

Шесть правил создания в ent#

  • client.User.Create() вызывается без аргументов: контекст живёт в Save/Exec/ID, а не в конструкторе билдера.
  • Обязательные поля и валидаторы проверяются до базы: ValidationError с именем поля и сообщением «missing required field».
  • Значения-указатели из API отдавайте SetNillable: nil тихо пропустится; у Nillable-полей SetNillable не нужен — там SetX уже принимает указатель.
  • Дубли ловите в два слоя: ent.IsConstraintError для констрейнтов вообще, sqlgraph.IsUniqueConstraintError для дублей; текст драйвера сохранён в Unwrap.
  • Пакет — CreateBulk или MapCreateBulk: один INSERT, все-или-ничего; upsert на пакете ставится на билк-билдер, а не на отдельные билдеры.
  • Связи при создании — AddGroups/AddGroupIDs: ent сам откроет транзакцию и покатит узлы и join-строки вместе.

Путь одной вставки

Соберите жизненный цикл client.User.Create()...AddGroups(...).Save(ctx) от конструктора до возврата.

Строки

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

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

Соберите upsert и пакет

Впишите цель конфликта, вариант «обнови из вставки», пакетный конструктор из среза и проверку именно на дубль.

// upsert: нет — вставь, есть — обнови (PostgreSQL): err := client.User. Create(). SetEmail("anna@entgo.io"). SetName("anna"). OnConflict(user.FieldEmail). // цель конфликта Update(). // значения из вставки Exec(ctx) // пакет из среза имён: users, err := client.User.(names, func(c *ent.UserCreate, i int) { c.SetName(names[i]) }).Save(ctx) // дубль именно уникального индекса: if sqlgraph.Is(err) { // email занят — вернём форме понятную ошибку }

Что дальше#

Писать мы научились — но вставленные строки нужно доставать обратно. Девятый урок — 09-zaprosy «Запросы: Query builder»: client.User.Query(), предикаты из ent/user, All, Only, First и Get по идентификатору — тот словарь, который мы уже щупали в самом первом уроке на примере NameEQ. Сегодняшний арсенал вам там пригодится сразу: Only и First бросают ошибки по своим правилам, и вы уже умеете читать классификацию ent. Полный план из 25 уроков — на странице курса. Домашнее задание переводит формы из урока в пальцевую память — пройдите его на своей базе.

Карточки: создание сущностей

Термин

client.User.Create() — что возвращает

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

*UserCreate — билдер с UserMutation (OpCreate) внутри. Аргументов нет: ни ctx, ни структуры. Контекст передаётся в Save(ctx)/SaveX/Exec/ExecX. Билдер можно собрать заранее, дополнить в функции и исполнить позже.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • client.User.Create() не принимает ни контекста, ни готовой структуры — он возвращает *UserCreate, билдер с UserMutation (OpCreate) внутри. Контекст передаётся на исполнении: Save(ctx), SaveX, Exec, ExecX. Формы Create(ctx, *User) в ent нет — поля перекладываются сеттерами, и компилятор проверяет каждое.
  • Четыре метода исполнения — два контракта: Save → (*User, error) и Exec → error обрабатывают ошибку; SaveX и ExecX паникуют и уместны в запуске, тестах и CLI, но не в HTTP-хендлерах.
  • Порядок этапов Save: defaults() (created_at из time.Now) → хуки → check() — обязательные поля и валидаторы до всякого SQL → sqlgraph: INSERT. Ошибка валидации — ValidationError с именем поля.
  • SetNillable-сеттеры («avoid nil checks») генерируются для Optional и Default-полей: SetNillableWebsite(*string) при nil молча пропускает поле. У Nillable-полей отдельный SetNillable не генерируется — там SetX уже принимает указатель.
  • Связи при создании: AddGroups/AddGroupIDs на обратной половине M2M (канон AddUsers из шестого урока — с другой стороны). Внешние рёбра включают авто-транзакцию: узел и join-строки катятся вместе, откат — на любой ошибке.
  • CreateBulk(builders...) и MapCreateBulk(slice, fn) → ([]*User, error): один INSERT с несколькими VALUES, выравнивание колонок NULL-ами, все-или-ничего, «inconsistent id values», если ID задан не у всех билдеров.
  • OnConflict()/OnConflictColumns(...) переводит цепочку в *UserUpsertOne: UpdateNewValues() (Immutable исключены), Update(func(u *ent.UserUpsert)) — SetX/UpdateX/AddX/ClearX, Ignore(), DoNothing() (SQLite/PG), ID(ctx) — id вставленной или обновлённой строки. PostgreSQL требует цель конфликта; MySQL — ON DUPLICATE KEY с LAST_INSERT_ID-трюком. Upsert — один стейтмент: только create-хуки.
  • Дубли: sqlgraph.IsUniqueConstraintError матчит тексты драйверов («Error 1062», «violates unique constraint», «UNIQUE constraint failed»), сгенерированный код оборачивает констрейнт-сбои в ent.ConstraintError («ent: constraint failed: ...», Unwrap сохраняет оригинал). Отмена контекста проходит насквозь: errors.Is(err, context.Canceled).

Домашнее задание

Пройдите все формы урока на своей базе из седьмого урока (флаг sql/upsert включён). Первое: создайте anna сеттер-стилем, затем повторите вызов с тем же email и разберите текст ошибки — найдите в нём имя индекса, прогоните ошибку через ent.IsConstraintError и sqlgraph.IsUniqueConstraintError и убедитесь, что вернули их по-разному. Второе: соберите трёх пользователей через MapCreateBulk из среза имён и уберите один из email-ов на дубль — проверьте, что ни одна строка не вставлена. Третье: выполните upsert-цепочку с UpdateNewValues на существующем email — сверьте, что name обновился, а created_at остался прежним (Immutable-поле исключено). Четвёртое: на PostgreSQL уберите цель конфликта из OnConflictColumns и получите родную ошибку про matching constraint. Пятое: создайте пользователя с AddGroupIDs сразу в двух группах и найдите в логах базы одну транзакцию с INSERT в users и двумя в group_users. Вопрос на подумать: почему upsert исполняется одним стейтментом и какие хуки при нём сработают, а какие — нет?

Личные заметкиПусто
Хранятся только в вашем браузере (ent · Создание сущностей)

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

Почему client.User.Create() не принимает контекст, если почти весь остальной API устроен наоборот?

Потому что Create() ничего не исполняет: он лишь возвращает билдер с мутацией внутри. Контекст нужен на исполнении — Save(ctx), Exec(ctx) или ID(ctx). Такое разделение даёт свободу: билдер можно собрать заранее, дополнить чужой функцией и исполнить позже, в другом контексте. Это же объясняет, почему у обычного create-билдера нет ID(ctx): вопрос «какой id вернулся» осмыслен и для обновлённой строки — он появляется только после OnConflict(), вместе с другим типом билдера.

Почему OnConflict() без колонок работает на MySQL, но падает на PostgreSQL?

ent рендерит upsert по-разному: для MySQL это ON DUPLICATE KEY UPDATE — конструкция без цели конфликта по определению; для PostgreSQL и SQLite — ON CONFLICT, где для DO UPDATE цель обязательна. Документация фиксирует: «In PostgreSQL, the conflict target is required». Решение — OnConflictColumns(user.FieldEmail) или SQL-API: sql.ConflictColumns(...) для колонок и sql.ConflictConstraint(...) для именованного констрейнта. Для DO NOTHING цель не нужна, но MySQL всё равно не умеет — ent подменяет его Ignore-вариантом.

Как надёжно отличить «email занят» от другой ошибки базы?

Двумя слоями. Сначала ent.IsConstraintError(err) — ошибки на errors.As: она поймает обёртку ent.ConstraintError, в которую сгенерированный код заворачивает любые нарушения констрейнтов, включая уникальность и внешние ключи. Затем sqlgraph.IsUniqueConstraintError(err) — она проверяет текст ошибки на маркеры конкретных диалектов: «Error 1062» у MySQL, «violates unique constraint» у PostgreSQL, «UNIQUE constraint failed» у SQLite. Исходный текст драйвера не теряется — он внутри ConstraintError через Unwrap, поэтому в лог попадает и имя индекса, и значение дубликата.

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

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

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

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

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

go-kit25 уроков~14 чпродвинутый25 уроков уже доступны

go-kit: микросервисный тулкит

Курс go-kit: endpoint, транспорт, логирование, метрики, трейсинг, circuit breaker, service discovery. 25 уроков с итоговым микросервисом.

  • Понимать архитектуру endpoint → transport → service
  • Оборачивать сервисы middleware: логи, метрики, трейсинг
  • Подключать HTTP и gRPC транспорты
  • Ставить rate limiting и circuit breaker