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

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

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

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

Удаление

Восьмой урок создал строки, тринадцатый научил их менять — остался последний жест жизненного цикла. Сегодня — ent delete: два билдера, DeleteOneID по ключу и Delete().Where() по предикатам из десятого урока, разные контракты возврата и счёт удалённых строк. Главный сюжет — каскадное удаление ent: что происходит с постами, членством в группах и дружбами, когда удаляется пользователь, и почему всё решают констрейнты из пятого урока, а не рантайм. Затем — мягкое удаление ent своими руками: DeletedAt-магии здесь нет, как нет её и в классических ORM, и вы соберёте её из поля, мутатора и предиката — по канону курса: анна, борис, вера, группа go-ninja и таблицы group_users, user_friends.

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

Маршрут урока — от одного DELETE до полной политики прощания. Первый сюжет — два билдера: точечный UserDeleteOne и массовый UserDelete, их контракты Exec и счёт удалённого. Второй — каскадное удаление ent: карта OnDelete из пятого урока со стороны исполнения — что база делает с постами при удалении анны и кто чистит строки в group_users, служебной таблице из шестого урока. Третий — отказ: когда внешний ключ блокирует DELETE, как выглядит ConstraintError и в каком порядке удалять руками. Четвёртый — мягкое удаление: поле deleted_at, предикат DeletedAtIsNil и мост к тринадцатому уроку — «удалить» значит «обновить». Финиш — чеклист и таблица выбора между жёстким и мягким путями. Данные прежние: анна (26 лет, два поста — «про рёбра» и «черновик»), борис (19), вера (17), группа go-ninja.

ent delete: два билдера и их контракты#

Удаление живёт в сгенерированном user_delete.go, и там два строения — ровно как в обновлении из тринадцатого урока. UserDeleteOne — точечная форма: client.User.DeleteOneID(id) или DeleteOne(u) по готовой сущности; Save здесь не нужен: Exec(ctx) сразу исполняет DELETE и возвращает только ошибку. UserDelete — массовая форма: client.User.Delete() без аргументов, Where принимает предикаты, а Exec(ctx) возвращает пару (int, error) — число удалённых строк. Это главный разворот ent delete: точечная форма не знает, что ответить на «сколько», и потому докладывает иначе — удалил и молчит либо вернул ошибку. Оба билдера собирают мутацию с предикатами и исполняются на Exec; ExecX-близнецы, паникующие вместо ошибок, здесь тоже сгенерированы, но в сервисном коде держитесь ошибки. Массовая форма без Where не упадёт: DELETE без WHERE по констрейнту SQL легален и стирает таблицу целиком — та же ответственность, что у забытого Where в тринадцатом уроке, только цена выше: обновление хотя бы оставило строки, пусть и с испорченными полями. И помните каталог: любой предикат из десятого урока — AgeLT, BioIsNil, HasPosts — работает и в Delete, потому что Where массовой формы принимает те же UserSelect-комбинаторы.

Обе формы удаления

GO
// точечно: удалить анну по ключу — только ошибка в ответе:
err := client.User.
        DeleteOneID(1).
        Exec(ctx)
if ent.IsNotFound(err) {
        // строки с id 1 не было: *NotFoundError
}

// массово: удалить всех несовершеннолетних — получим счёт:
n, err := client.User.
        Delete().
        Where(user.AgeLT(18)).
        Exec(ctx)
fmt.Println(n) // сколько строк база реально удалила

DeleteOneID(1).Exec(ctx) — error-only контракт: либо строка ушла, либо ошибка, включая NotFoundError для отсутствующего ключа. Delete().Where(user.AgeLT(18)) — массовая форма: тот же предикат, что served бы Query, и возврат числа. Вера с её 17 покинет базу, n станет единицей. Chain-форма идентична обновлению — Delete() возвращает билдер, Where ничего не исполняет, всё происходит на Exec.

Проверка: контракты Exec

Что возвращают Exec у точечной и массовой форм удаления?

Сколько строк удалил предикат

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

// анна 26 лет, борис 19, вера 17.
n, err := client.User.Delete().
        Where(user.AgeLT(20)).
        Exec(ctx)

// чему равно n?

Ноль строк, NotFoundError и отсутствие мягкого режима#

Три исхода массового удаления стоит различать на автомате. n > 0 — строки ушли. n = 0 и err = nil — предикат не попал ни в одну строку: это данные, не сбой, и обрабатывать стоит как нормальный путь (в отличие от HTTP, где DELETE на несуществующий ресурс часто отвечает 404, ent массовой формой честно доложит нулём). NotFoundError — привилегия точечной формы: DeleteOneID на отсутствующем ключе вернёт *NotFoundError, которую ловит ent.IsNotFound из девятого урока. Четвёртого исхода — «удалил, но не совсем» — в ent не бывает: Exec исполняет ровно один DELETE, никаких тихих режимов и отложенных пометок в рантайме не существует. Если вам нужна отсрочка — это не delete, а мягкое удаление, и дальше мы соберём его руками. Отдельно про MaskNotFound из девятого урока: он глушит именно «не найдено», превращая ошибку в nil, — удобно в очистителях, где повторный вызов по уже удалённому ключу является нормой, а не инцидентом. В логе драйвера всё видно дословно: один DELETE ... WHERE id = ? для точечной формы, один DELETE ... WHERE <предикаты> для массовой — ent не добавляет к удалению ни подтаблиц, ни второго запроса.

Каскадное удаление ent: кто стирает соседей#

Вот главный вопрос урока: удаляем анну — что происходит с её постами, членством в go-ninja и дружбами? ent здесь не рантайм-менеджер, а генератор констрейнтов: на исполнении всё решает база по referential action, который вы объявили в схеме. Пятый урок дал полную карту: ребро Optional по умолчанию живёт с ON DELETE SET NULL, ребро Required — с NO ACTION, а аннотация entsql.OnDelete переопределяет действие на CASCADE, RESTRICT, SET NULL или SET DEFAULT. Наш канон собран по этой карте: ребро posts несёт entsql.OnDelete(entsql.Cascade) — «каскад из пятого урока», как назвал его шестой, — и удаление анны стирает оба её поста руками базы, без единой строки вашего кода. Служебные M2M-таблицы каскадят сами по себе: ent ставит CASCADE на обе колонки group_users и user_friends, не спрашивая, — строки членства и дружб исчезают вместе с пользователем, а группы остаются жить: связь умерла, данные целы. Единственный просвет — through-таблица из шестого урока: там дружба — полноценная сущность со своими рёбрами, Required по построению, и без аннотации Cascade на обоих рёбрах база ответит отказом. Так что каскадное удаление ent делает чужими руками: вашими — в схеме, базовыми — на исполнении.

SQL-след: один DELETE и работа базы

GO
// удаляем анну — один стейтмент от ent:
err := client.User.DeleteOneID(1).Exec(ctx)

// SQL-след (упрощённо):
// DELETE FROM users WHERE id = 1
//
// дальше работает база, а не ent:
//   ON DELETE CASCADE на posts.author_id
//     -> DELETE FROM posts WHERE author_id = 1   (оба поста анны)
//   ON DELETE CASCADE на group_users.user_id
//     -> строки членства в go-ninja исчезли
//   ON DELETE CASCADE на user_friends.user_id
//     -> дружбы исчезли; группы и борис с верой — целы

След короток: ent исполняет ровно один DELETE — никакой ORM-магии по подтаблицам в коде ent нет. Каскады делает движок базы по констрейнтам, которые нагенерировал ent из схемы: посты анны стёр CASCADE с ребра posts, строки членства и дружб — автоматические CASCADE-колонки служебных таблиц. Группы остались: у связи не стало владельца, у данных не стало причины умирать. Если бы ребро posts было без аннотации, дефолт для Optional — SET NULL: посты остались бы сиротами с author_id = NULL, как расписано в пятом уроке.

Проверка: судьба соседей анны

Схема канона: ребро posts — с entsql.OnDelete(entsql.Cascade), группы — служебная m2m, дружбы — through-таблица с явным CASCADE. Что произойдёт после DeleteOneID(анна)?

Когда база отказывает: NO ACTION и ConstraintError#

Каскад — выбор, а не дефолт. Ребро Required без аннотации получает NO ACTION, аннотация entsql.OnDelete(entsql.Restrict) отказывает намеренно — и в обоих случаях DELETE родителя с детьми падает ошибкой внешнего ключа. ent оборачивает её в *ConstraintError — ту же ошибку, что ловили на дубликатах уникальности в восьмом уроке, — проверяется хелпером ent.IsConstraintError. Это не сбой соединения и не баг: база исполнила контракт схемы, и логика приложения обязана понять отказ. Два пути. Первый — изменить схему: если судьба детей при смерти родителя определена, это должен быть referential action, а не код — Cascade для «контент умирает с аккаунтом», SetNull для «контент переживает аккаунт», Restrict для «случайных удалений не должно быть». Второй — ручной порядок удаления: сначала дети отдельным Delete() с предикатом, потом родитель — он же порядок в коде, и тут возникает соблазн «полуудалённого состояния», если между двумя DELETE что-то упало. Именно для таких пар операций существуют транзакции: в шестнадцатом уроке вы завернёте «детей, потом родителя» в client.Tx, где частичный успех невозможен, — а пока просто держите порядок жёстким и обрабатывайте ConstraintError как штатную ветку, а не как 500.

Ручной порядок и ветка отказа

GO
// снести дружбы анны до неё самой (through-таблица без Cascade):
_, err := client.Friendship.Delete().
        Where(friendship.HasUserWith(user.ID(1))).
        Exec(ctx)
if err != nil {
        return err
}

// теперь родитель:
err = client.User.DeleteOneID(1).Exec(ctx)
if ent.IsConstraintError(err) {
        // FK всё ещё держит: кто-то ссылается — разберитесь в карте рёбер
        return fmt.Errorf("удаление блокировано ссылками: %w", err)
}
if err != nil {
        return err
}

Порядок «сначала дети, потом родитель» — прямое следствие NO ACTION: база не угадает за вас. HasUserWith — рёберный предикат из десятого урока, он отбирает строки дружбы по владельцу. ent.IsConstraintError — штатная проверка отказа внешнего ключа: после неё обычно логируют карту зависимостей, а не ретраят вслепую. Правильнее всего один раз поправить схему — аннотация Cascade на рёбрах through-таблицы убирает весь этот код, о чём честно предупреждал шестой урок.

Чеклист удаления перед продом#

  • Для каждого ребра схемы known-ответ: что будет с детьми — Cascade, SetNull, Restrict или дефолт? Карта есть в пятом уроке; если ответа нет — он найдётся в проде.
  • Массовые удаления идут предикатом из десятого урока и с прицелом: Count с тем же Where перед Exec — как и для обновления из тринадцатого.
  • ConstraintError — штатная ветка, не 500: логируйте, какая ссылка держит строку, и решайте в коде, а не ретраями.
  • Перед жёстким удалением решите, что нужно «на память»: сущность не вернётся из Exec — читайте до, с With-цепочкой из одиннадцатого урока.
  • Опасные пути — за транзакциями из шестнадцатого урока: пара «дети, потом родитель» обязана быть атомарной.

Мягкое удаление ent: руками из поля и предиката#

GORM в девятом уроке своего курса показывал gorm.DeletedAt: поле объявлено — и все выборки автоматически получают IS NULL, а Delete превращается в UPDATE. В ent такой магии нет, и это осознанное решение: удаление ent удаляет, а маскировку вы собираете сами — из инструментов, которые уже есть. Рецепт из трёх деталей. Первая: в схему Optional-поле deleted_at с типом field.Time из третьего урока. Вторая: «удаление» — это обновление из тринадцатого урока: UpdateOneID(id).SetDeletedAt(time.Now()) — строка остаётся, история сохраняется, откат возможен руками. Третья: живые отбираются предикатом user.DeletedAtIsNil() — семейство IsNil из каталога десятого урока — и этот предикат обязан сопровождать каждый читающий запрос: забудете один Query — «удалённый» пользователь всплывёт в ленте. Именно здесь обычно приходят к интерсепторам восемнадцатого урока, которые вшивают фильтр во все запросы схемы, — но ручная версия должна быть написана и понята раньше автоматической. Оговорка про уникальность: email с UniqueConstraint из третьего урока не даст «удалённому» пользователю зарегистрироваться заново — уникальный индекс видит строку, даже если ваш код нет; лечится частичным индексом на живых строках, тема двадцатого урока. Мягкое удаление ent — это политика на вашей стороне, и в этом его сила и его цена.

Soft delete: схема, удаление, живые

GO
// в схеме User (третий урок):
field.Time("deleted_at").
        Optional().
        Nillable()

// «удалить» = обновить (тринадцатый урок):
err := client.User.
        UpdateOneID(1).
        SetDeletedAt(time.Now()).
        Exec(ctx)

// живые — только с фильтром:
live, err := client.User.Query().
        Where(user.DeletedAtIsNil()).
        Order(ent.Asc(user.FieldID)).
        All(ctx)

// восстановить — снова обновление:
err = client.User.
        UpdateOneID(1).
        ClearDeletedAt().
        Exec(ctx)

Поле Optional + Nillable из четвёртого урока даёт честную тройку состояний: никогда не удалялся (nil), удалён и известен момент (значение), восстановлен (ClearDeletedAt вернул NULL). SetDeletedAt — обычный Set-мутатор, никакого отдельного API «мягкого удаления» в ent нет, и это упрощает лог: в следе видно ровно то, что происходит — UPDATE вместо DELETE. DeletedAtIsNil обязан стоять в каждом читающем запросе: пропущенный один Query — и вера, «удалённая» вчера, снова в каталоге.

Проверка: механика мягкого удаления

Пользователь «удалён» через UpdateOneID(id).SetDeletedAt(time.Now()). Что вернёт client.User.Query().Where(user.DeletedAtIsNil()).Count(ctx) при трёх живых пользователях и одном помеченном?

Соберите оба пути удаления

Впишите точечный билдер удаления, хелпер ошибки внешнего ключа и предикат живых строк.

// 1. жёстко удалить по id: err := client.User.(id). Exec(ctx) // 2. поймать отказ внешнего ключа: if ent.(err) { return fmt.Errorf("строку держат ссылки: %w", err) } // 3. показать только живых при мягком удалении: users, err := client.User.Query(). Where(user.()). All(ctx)

Путь одного DeleteOneID

Соберите жизненный цикл жёсткого удаления анны: что происходит и в каком порядке от вызова до пустого лога.

Строки

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

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

Карта удалений: форма, след, судьба соседей#

СценарийAPISQL-следЧто с соседями
Точечное по ключуDeleteOneID(id).Exec(ctx)DELETE ... WHERE id = ?база решает по OnDelete
Массовое по предикатуDelete().Where(...).Exec(ctx)DELETE ... WHERE <предикаты>каскады по каждой строке
Ребро Optional, без аннотации—констрейнт ON DELETE SET NULLдети-сироты с NULL-ссылкой
Ребро Required, без аннотации—констрейнт NO ACTIONDELETE блокируется ConstraintError
Аннотация entsql.OnDelete(entsql.Cascade)—констрейнт ON DELETE CASCADEдети стираются базой (канон posts)
Служебная m2m-таблица—CASCADE на обеих колонкахстроки связи исчезают, узлы целы
Through-таблица без аннотаций—NO ACTION на её рёбрахчистить руками или аннотировать
Мягкое удалениеUpdateOneID(id).SetDeletedAt(...)UPDATE ... SET deleted_atсоседи не тронуты вовсе
ВосстановлениеUpdateOneID(id).ClearDeletedAt()UPDATE ... SET deleted_at = NULLживость возвращает предикат IsNil

Что дальше#

Вы умеете прощаться с данными правильно: точечная и массовая формы с разными контрактами, счёт удалённого, карта каскадов из пятого урока со стороны исполнения, ручной порядок с ConstraintError и мягкое удаление из поля, мутатора и предиката — мост между тринадцатым уроком и этим. Удаление завершает CRUD-кольцо курса: создание, обновление, удаление — все три операции собираются из одних и тех же билдеров и отвечают компилятору за каждый вызов. Дальше — мир вокруг него: пятнадцатый урок разберёт миграции Schema.Create и atlas — как схемы из ваших файлов вообще оказываются констрейнтами в базе, — а шестнадцатый завернёт составные операции в транзакции. Домашнее задание ниже проведёт оба пути удаления по вашей базе и заставит прочитать лог как улику. Полный план из 25 уроков — на странице курса, предикаты для Where — в десятом уроке, страницы для списков — в двенадцатом.

Карточки: удаление

Термин

Две формы удаления и их контракты

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

DeleteOneID(id).Exec(ctx) — error-only: строка ушла или *NotFoundError (ent.IsNotFound). Delete().Where(...).Exec(ctx) — (int, error): число удалённых строк; n = 0 — данные, не сбой. ExecX-близнецы паникуют; сущности ни одна форма не возвращает — читайте до удаления.

1 / 6

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

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

Вывод

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

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

  • ent delete — два сгенерированных билдера: DeleteOneID по ключу (Exec → error-only, пустой ключ — *NotFoundError) и Delete().Where() по предикатам десятого урока (Exec → (int, error), число удалённых). n = 0 — нормальный путь, не ошибка.
  • Каскадное удаление ent делает не рантайм, а база: ent генерирует referential actions из схемы, пятый урок дал карту — Optional → SET NULL, Required → NO ACTION, аннотация entsql.OnDelete переопределяет на Cascade/Restrict/SetNull/SetDefault.
  • Канон курса: ребро posts несёт entsql.OnDelete(entsql.Cascade) — удаление анны стирает её посты одним DELETE; служебные m2m-таблицы каскадят на обеих колонках всегда — строки членства и дружб исчезают, группы и узлы целы.
  • Through-таблица — обычная сущность: её Required-рёбра без аннотаций дают NO ACTION, и удаление участника блокируется до чистки или аннотации Cascade.
  • Отказ FK — *ConstraintError (ent.IsConstraintError), штатная ветка логики: либо поправить схему, либо удалить детей раньше родителя; составные удаления — под транзакциями шестнадцатого урока.
  • Мягкое удаление ent собирается руками: поле deleted_at (Optional, Nillable), удаление = UpdateOneID(id).SetDeletedAt(time.Now()) — мост к тринадцатому уроку, живые = Where(user.DeletedAtIsNil()), восстановление = ClearDeletedAt.
  • Уникальность против мягкого удаления: UniqueConstraint видит помеченные строки — повторная регистрация упадёт; лечится частичным индексом на живых, тема двадцатого урока. Автофильтра как у GORM DeletedAt в ent нет — и каждый читающий Query обязан нести предикат.
  • Удаление завершает CRUD-кольцо курса: Create из восьмого, Update из тринадцатого, Delete из этого урока — одна и та же билдерная философия, компилятор и схема как страховые полисы.

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

Работайте на базе из восьмого урока — анна, борис, вера, их посты, группа go-ninja — и включите лог SQL драйвера. Первое: удалите анну DeleteOneID и прочитайте след: один DELETE, затем работа базы — постов анны в таблице быть не должно, строки group_users обнулились по её ключу, группы целы. Второе: снимите аннотацию Cascade с ребра posts, перегенерируйте и повторите удаление на свежей копии базы: без аннотации Optional-ребро даст SET NULL — посчитайте сирот с author_id = NULL и опишите словами, чем этот мир отличается от каскадного. Третье: соберите through-сценарий шестого урока — попытайтесь удалить пользователя с дружбами без аннотаций, поймайте ent.IsConstraintError и запишите, какая ссылка держала строку; затем решите: чистить дружбы до родителя или аннотировать Cascade — и объясните выбор. Четвёртое: добавьте deleted_at в схему, «удалите» веру через SetDeletedAt и проверьте три подсчёта: Count без предиката — четыре, с DeletedAtIsNil — два, с DeletedAtNotNil — один; затем восстановите её ClearDeletedAt. Пятое: сделайте массовое удаление с прицелом — Count с тем же Where до Exec — на предикате AgeLT(20) и сверьте числа; затем выполните Delete() без Where на копии базы и зафиксируйте в комменте, сколько строк стоила забытая строка кода. Вопрос на подумать: почему ent не сделал мягкое удаление флагом схемы, и какие две самые дорогие ошибки вы бы получили с автоматическим IS NULL-фильтром?

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

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

DeleteOneID и DeleteOne — когда какой?

DeleteOneID(id) берёт первичный ключ — путь для обработчиков, где id пришёл из маршрута. DeleteOne(u) принимает готовую сущность и использует её ID — удобно в циклах, где объект уже на руках из Query. Разницы в SQL нет: оба строят DELETE по ключу и отвечают error-only контрактом; NotFoundError ожидайте в обоих случаях, если строку уже удалили конкурентно.

Как узнать, что именно удалил массовый Delete?

Ни срез сущностей, ни их id Exec не вернёт — только число. Если состав важен, прочитайте выборку до удаления тем же предикатом: Query().Where(...).All(ctx) с With-цепочкой из одиннадцатого урока, если нужны соседи, — затем Delete с тем же Where. Для аудита «кто именно ушёл» помогает мягкое удаление из этого урока: пометка вместо стирания сохраняет состав навсегда.

Почему ent не фильтрует мягко удалённые строки автоматически?

Потому что у ent нет поля-конвенции: deleted_at может называться иначе, быть временем или флагом, а правила живости — бизнес-логикой. GORM стандартизировал gorm.DeletedAt и вшивает IS NULL в каждый запрос; ent оставляет предикат вам — DeletedAtIsNil из десятого урока, а автоматизацию отдают интерсепторам восемнадцатого. Так вы сами решаете, в каких запросах помеченные строки живы: в админке — всегда, в каталоге — никогда.

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

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

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

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

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

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

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

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

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