---
title: "Optional и Nillable поля"
description: "ent optional nillable: NULL против нулевых значений Go, SetBio, SetNillableBio и ClearBio, предикаты IsNil — выбираем стратегию для nullable полей."
course: "ent: графовая ORM с кодогенерацией"
url: "https://golangmaster.ru/courses/ent/lessons/04-optional/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# Optional и Nillable поля

*Курс «ent: графовая ORM с кодогенерацией», урок 4 из 25. ~16 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/ent/lessons/04-optional/*

Тема урока — ent optional nillable: полная NULL-семантика ent, которую мы отложили в конце прошлого урока, где bio получил пару Optional().Nillable(). Разбираемся, чем nullable поле отличается от нулевого значения Go, что генерируется рядом с SetBio — SetNillableBio и ClearBio, — как искать записи без значения и как выбрать одну из трёх стратегий пустоты для микроблога.

В [третьем уроке](/courses/ent/lessons/03-polya/) мы объявили bio как MaxLen(280).Optional().Nillable() и ушли со сцены на самом интересном месте: что ent делает с NULL. Сегодня разбираем пару Optional и Nillable до дна. Тема урока, ent optional nillable, закрывает три вопроса: где в Go прячется нулевое значение и почему оно конфликтует с NULL из SQL, какие методы SetBio, SetNillableBio и ClearBio генерирует ent и как находить записи без значения. Стратегий пустоты всего три, и микроблог даст каждой работу: bio останется указателем, для сайта пользователя заведём поле без Nillable, а роль уже живёт с Default. Имена методов, которые встретятся ниже, никто не набирает руками — их выдаёт go generate ./ent после правки схемы, как договорились ещё во [втором уроке](/courses/ent/lessons/02-shemy/).

### NULL в SQL и нулевое значение Go: фундаментальный разрыв

NULL в SQL — это не «пусто», а «значения нет и не было»: биография не заполнена, срок не поставлен, ник ещё не выбран. У Go позиция другая: каждый тип имеет нулевое значение, которое нельзя отключить — 0 для чисел, false для bool, пустая строка, 1 января нулевого года для time.Time. Две картины мира сталкиваются в момент чтения строки из базы: драйвер database/sql достаёт колонку через обёртку sql.NullString с флагом Valid, а сгенерированный код ent переводит результат на язык структуры. Правило перевода короткое: у Nillable-полей NULL становится nil, у обычных — нулевым значением типа. Ту же задачу мы видели в [уроке про sqlc типы](/courses/sqlc/lessons/04-tipy/): там nullable колонки превращаются в sql.NullString и pgtype с тем же флагом Valid — ent выбрал третий путь, указатели. Так себя вели и классические ORM: [в вводном уроке GORM](/courses/gorm/) нулевые значения и «нет данных» путались в одном поле точно так же. Разница между «в базе NULL» и «в базе пустая строка» переживает это путешествие только при одной из стратегий — в этом весь сюжет урока.

### Нулевые значения Go: что читается вместо NULL

| Тип поля | Нулевое значение | Чем грозит путаница с NULL |
| --- | --- | --- |
| string | "" (пустая строка) | «не рассказал о себе» неотличимо от «рассказал пустоту» |
| int | 0 | пропущенный возраст выглядит как настоящий ноль |
| bool | false | «не подтвердил» и «не спрашивали» сливаются в одно |
| time.Time | 0001-01-01 00:00:00 UTC | нулевая дата пробивается в отчёты и API-ответы |
| []byte | nil | почти безобидно: len 0 и «нет значения» ведут себя одинаково |
| uuid.UUID | 00000000-0000-0000-0000-000000000000 | нулевой токен может уйти наружу как настоящий |

### Стратегия 1: ent optional nillable — указатель вместо обёртки

Первая стратегия — та, по которой построено bio: field.String("bio").Optional().Nillable(). Optional снимает обязательность: SetBio можно не вызывать, Save не вернёт missing required field, а колонка становится nullable. Nillable меняет тип в сгенерированной структуре на *string: ent читает NULL и кладёт в Bio nil, читает значение — кладёт указатель на строку. Проверка u.Bio == nil — честный вопрос «в базе есть что-нибудь?», и ответ больше не подменяется пустой строкой.

Когда указатели эргономичны: для строк, времени и любых типов, где «пустота» — отдельное состояние со своим смыслом. Био, дата публикации, дата блокировки, токен — все они бывают «не заданы», и каждое из этих состояний нужно различать. Когда указатели избыточны: для счётчиков и bool, где нулевое значение редко несёт отдельный смысл, а разыменование *u.Bio захламляет код. Цена стратегии не нулевая и на границе API: nil-указатель требует решения, что отдавать клиенту — null, пустую строку или вовсе убрать поле из ответа. Этот выбор разберём на блоке про JSON.

### bio, website, role: три стратегии в схеме

```go
// Стратегия 1: Optional + Nillable — «нет био» видно в коде как nil
field.String("bio").
        MaxLen(280).       // лимит из прошлого урока
        Optional().        // SetBio можно не вызывать, колонка NULL-able
        Nillable(),        // в структуре Bio *string

// Стратегия 2: только Optional — пусто и «нет значения» значат одно
field.String("website").
        Optional(),        // колонка NULL-able, в структуре Website string

// Стратегия 3: Default вместо Optional — «пусто» запрещено как класс
field.Enum("role").
        Values("user", "moderator", "admin").
        Default("user"),   // колонка NOT NULL, роль есть всегда
```

Три поля микроблога — три ответа на вопрос «что делать, когда значения нет». bio различает пустоту и отсутствие; website объединяет их: пустой сайт и «нет сайта» — одно состояние; role не допускает отсутствия — вместо пустоты ent подставит user. В таблице describe из [первого урока](/courses/ent/lessons/01-znakomstvo/) это видно сразу: у bio Optional true, Nillable true, Default false; у website только Optional true; у role Default true при Optional false. Колонки bio и website в базе одинаково NULL-able — различие стратегий живёт не в SQL, а в Go-коде, и следующий блок это покажет.

### Что окажется в user.go: три версии поля

```go
// Стратегия 2: только Optional
type User struct {
        Website string `json:"website,omitempty"` // NULL читается как ""
}

// Стратегия 1: Optional + Nillable
type User struct {
        Bio *string `json:"bio,omitempty"` // NULL читается как nil
}

// Стратегия 3: Default вместо Optional
type User struct {
        Role Role `json:"role,omitempty"` // NULL не встречается: колонка NOT NULL
}
```

Разница живёт не в базе — колонки website и bio одинаково nullable — а в типе Go. Website string: NULL из базы превращается в пустую строку, и «пользователь без сайта» неотличим от «пользователя с пустым сайтом» — мы сами решили, что это одно и то же. Bio *string: nil означает ровно «в базе NULL». Role — именованный строковый тип с константами RoleUser, RoleModerator, RoleAdmin из прошлого урока; колонка NOT NULL, и вопроса «а что если NULL» не существует. Обратите внимание на json-теги: ent по умолчанию пишет всем полям omitempty, поэтому пустая строка и nil-указатель просто исчезнут из JSON-ответа; если API-контракт требует явный null, тег переопределяется через StructTag — приём покажем ниже.

### Проверка: чтение NULL без Nillable

**Вопрос:** bio объявлен только Optional(), без Nillable: в структуре Bio string. В базе у пользователя колонка bio = NULL. Что окажется в u.Bio после client.User.Get(ctx, id)?

- nil: ent всегда читает NULL как nil
- ✅ "" — пустая строка: NULL превратился в нулевое значение типа
- sql.NullString: ent наружу отдаёт обёртки database/sql
- Ошибка сканирования: NULL нельзя прочитать в string

Сгенерированный код присваивает полю значение только когда sql.NullString.Valid равен true; при NULL присваивания нет — Bio остаётся нулевым значением, пустой строкой. nil был бы только с Nillable. Обёртки ent прячет внутри: наружу — указатели или значения. Ошибки тоже нет: sql.NullString спокойно читает NULL.

### Сгенерированные мутации: SetBio, SetNillableBio и ClearBio

Схема готова — смотрим, что ent сгенерировал для Optional-поля bio. Три рабочих метода. SetBio("...") записывает строку. SetNillableBio(p) принимает *string и устроен внутри честно: if v != nil { SetBio(*v) } — nil молча пропускается; формулировка документации ent так и звучит: «sets the field if the given value is not nil». ClearBio() помечает поле на очистку — Save отправит в базу NULL. Рядом есть ResetBio, но он не про базу: метод отменяет незавершённое изменение внутри мутации, значение в таблице он не трогает.

Теперь распределение по билдерам. У create-билдера ClearBio нет — при создании «не вызвал Set» уже означает NULL, чистить нечего. У update-билдеров — UserUpdate и UserUpdateOne — ClearBio есть и работает. Заметьте: SetNillableBio генерируется для любого Optional-поля, даже без Nillable — у website он тоже будет, с типом *string. А вот обязательным полям, включая role с Default, ни SetNillable, ни Clear не положены: колонка NOT NULL, там NULL не бывает.

### Паттерн SetNillableBio(input) в обработчике

```go
// input.Bio — *string из JSON-запроса профиля:
// клиент прислал null — в поле nil; прислал текст — указатель.

// создание: NULL, если био не прислали
client.User.Create().
        SetName("Анна").
        SetEmail("anna@example.com").
        SetNillableBio(input.Bio). // nil — поле останется NULL
        Save(ctx)

// обновление: заменить био, только если прислали новое
client.User.UpdateOneID(id).
        SetNillableBio(input.Bio). // nil — старое значение не трогаем
        Save(ctx)

// обновление: пользователь нажал «стереть био» — пишем NULL
client.User.UpdateOneID(id).
        ClearBio().                // не SetNillableBio(nil) — тот лишь no-op
        Save(ctx)

// чтение с проверкой на пустоту
if u.Bio != nil {
        fmt.Println("био:", *u.Bio)
}
```

Классический обработчик профиля. input.Bio — указатель *string из JSON: клиент прислал null — распаковка положит в поле nil, прислал текст — указатель на строку. SetNillableBio(input.Bio) покрывает оба случая одной строкой: nil тихо пропускается, значение записывается. При создании незаданное Optional-поле и так уйдёт в базу NULL; при обновлении no-op означает «не трогать старое значение». Третья мутация — намеренная очистка: ClearBio — единственный способ записать NULL в существующую строку. Чтение — с nil-проверкой: разыменование *u.Bio только после if u.Bio != nil, иначе паника.

### Что напечатает обновление с nil

```go
// bio в схеме: field.String("bio").Optional().Nillable()
// у пользователя id=42 bio = "пишу о Go"

client.User.UpdateOneID(42).
        SetNillableBio(nil).
        Exec(ctx)

u, _ := client.User.Get(ctx, 42)
fmt.Println(*u.Bio)
```

**Что выведет программа?**

- Паника: разыменование nil-указателя — Bio стал nil
- ✅ пишу о Go: SetNillableBio(nil) — no-op, значение не изменилось
- Ошибка Exec: nil нельзя передавать в SetNillableBio
- Пустая строка: поле очистилось, ent вернул ""

Сгенерированный SetNillableBio — это if v != nil { SetBio(*v) }: при nil вызова Set не происходит, мутация уходит без изменения bio, и в базе остаётся прежняя строка. Паники нет: Bio указывает на «пишу о Go». Ошибки тоже нет — nil для SetNillable легален и означает «не задавать». Чтобы записать NULL в существующую строку, нужен ClearBio.

> **SetBio("") — это не NULL.** Частый баг: «пользователь стёр био» обрабатывают как SetBio(""). В колонке окажется пустая строка, а не NULL: BioIsNil() такого пользователя не найдёт, а при Optional-без-Nillable чтении вы вообще не отличите стёртое от никогда не заполнявшегося. Правило: если в интерфейсе есть действие «удалить значение» — это ClearBio(), и только он. И наоборот: SetNillableBio(nil) при обновлении не очистит поле — это no-op, а не очистка.

Чтение — по типу поля. Сгенерированный код сканирует колонку через sql.NullString из database/sql и присваивает полю структуры значение только при Valid=true. Nillable-поле при NULL остаётся nil; обычное — нулевым значением: в Website окажется "". Отсюда два идиома чтения. Для bio — разыменование после проверки: if u.Bio != nil { fmt.Println(*u.Bio) }, иначе паника. Для website проверок нет по договорённости: пустая строка и «нет сайта» значат одно и то же, мы так спроектировали вторую стратегию.

Последний штрих — JSON. Дефолтный тег ent json:"...,omitempty" выбрасывает из ответа и пустую строку, и nil-указатель: API не покажет разницу между null и "". Если контракт требует явный null, переопределите тег: field.String("bio").StructTag(`json:"bio"`) — теперь nil придёт клиенту как null, а значение — как строка.

### Поиск NULL: предикаты BioIsNil и BioNotNil

Фильтр «найти пустые» упирается в особенность SQL: сравнение bio = NULL никогда не бывает истинным — неизвестность тянется и в равенство, и в неравенство, поэтому строка с NULL не пройдёт ни bio = NULL, ни bio <> NULL. У SQL есть отдельные операторы IS NULL и IS NOT NULL, а ent генерирует для каждого Optional-поля пару предикатов: user.BioIsNil() и user.BioNotNil(). В сгенерированном where.go за ними стоят sql.FieldIsNull и sql.FieldNotNull — прямой перевод IS NULL на язык билдера. Работают предикаты на уровне колонки, так что не важно, какая из двух стратегий у поля: BioIsNil найдёт и nil-указатель, и пустую строку, которой дублировали NULL. Цепочка client.User.Query().Where(user.BioIsNil()).Count(ctx) вернёт всех, кто био не заполнял. Обязательным полям предикаты не генерируются: у name NULL не бывает, и NameIsNil просто не существует.

### Проверка: фильтр пустых био

**Вопрос:** Нужно найти всех пользователей, у которых био не заполнено (bio: Optional+Nillable, некоторые стёрли его через ClearBio). Какой фильтр правильный?

- Where(user.BioEQ("")) — выбрать пустые строки
- Where(user.BioNEQ("")) — исключить пустые, остальное «заполнено»
- ✅ Where(user.BioIsNil()) — IS NULL, единственный честный способ
- Прочитать All(ctx) и отфильтровать u.Bio == nil в цикле

Стёртые через ClearBio значения — это NULL, а не пустая строка: BioEQ("") их не найдёт. BioNEQ("") тоже пропустит их мимо: сравнение с NULL в SQL даёт UNKNOWN, строка не проходит ни равенство, ни неравенство. Перебор сработает, но вытащит всю таблицу в приложение — расточительно. BioIsNil разворачивается в IS NULL и находит ровно то, что нужно.

### Соберите обработчик профиля

Впишите сеттер с указателем, метод очистки и предикат пустоты.

```go
// input.Bio — *string из JSON-запроса (null -> nil)
updated, err := client.User.UpdateOneID(id).
        ___(input.Bio). // прислали новое — записать; nil — не трогать
        Save(ctx)

// пользователь стёр био в интерфейсе — записываем NULL:
err = client.User.UpdateOneID(id).
        ___().
        Exec(ctx)

// и считаем всех, кто био так и не завёл:
n, err := client.User.Query().
        Where(user.___()).
        Count(ctx)
```

Подсказка: Сеттер для *string зовётся SetNillable..., очистка в update-билдере — Clear..., а «нет значения» ищет предикат ...IsNil().

Правильные ответы в пропусках: `SetNillableBio`, `ClearBio`, `BioIsNil`

### Жизненный цикл nullable-поля

Расставьте шаги от заполненного био до записи NULL и фильтра по пустоте.

Правильный порядок строк:

1. `SetBio("пишу о Go") при создании — Save(ctx) записал значение в колонку bio`
2. `UpdateOneID(id).SetNillableBio(nil) — no-op: nil пропускается, значение в базе прежнее`
3. `ClearBio() — поле помечается к очистке в текущей мутации`
4. `Save(ctx) — ent отправляет UPDATE users SET bio = NULL`
5. `Get(ctx, id) вернул пользователя с u.Bio == nil — Nillable прочитал NULL`
6. `user.BioIsNil() — теперь фильтр «без био» находит и его`

Optional хорошо дружит с Unique: пара field.String("handle").Optional().Unique() описывает ник, который либо уникален, либо отсутствует — пользователь ещё не придумал себе @handle. База такую комбинацию любит: в уникальном индексе PostgreSQL, SQLite и MySQL (InnoDB) разрешено сколько угодно NULL — они не конфликтуют, потому что NULL не равен ничему, даже другому NULL. Ограничение сработает только на двух users с одинаковым непустым ником. В прошлом уроке была обратная история: Default уникальным полям запрещён — автозначение мгновенно столкнулось бы само с собой на второй записи. Optional+Unique — легальная пара «уникален, если есть», и это стоит держать в голове, проектируя необязательные идентификаторы.

### Nillable ent без Optional: честное представление

Nillable умеет жить и без Optional — на обязательных полях. Сценарий: поле заполняется дефолтом, но при частичных выборках (о них — в будущих уроках про запросы) его может не быть в прочитанной строке. Тогда обычный time.Time честно покажет нулевую дату 0001-01-01T00:00:00Z — она же пробьётся в json.Marshal: в примере из документации ent пустая Task сериализуется как {"created_at":"0001-01-01T00:00:00Z"}. У Nillable-поля в той же ситуации nil: в коде «значения нет» видно, а с дефолтным тегом omitempty поле просто не попадёт в JSON. В прошлом уроке эта способность упоминалась одной строкой — теперь причина ясна: Nillable не про nullable колонку, а про честное представление «значения может не быть» в самом Go.

### Как выбрать стратегию: таблица решений

| Ситуация в микроблоге | Стратегия | Схема | Тип в Go | Мутации |
| --- | --- | --- | --- | --- |
| bio: «не рассказал» и «стёр» — разные состояния | Optional + Nillable | field.String("bio").Optional().Nillable() | *string | SetBio / SetNillableBio / ClearBio |
| website: пусто и «нет» — одно и то же | Optional | field.String("website").Optional() | string | SetWebsite / SetNillableWebsite / ClearWebsite |
| role: пустой роли не бывает | Default | field.Enum("role").Default("user") | Role (строка) | SetRole |
| created_at: время ставит ent | Default + Immutable | field.Time("created_at").Default(time.Now).Immutable() | time.Time | SetCreatedAt — только при создании |
| handle: уникальный ник или отсутствие | Optional + Unique | field.String("handle").Optional().Unique() | string | SetHandle / ClearHandle |
| token: API-ключ, которого может не быть | Optional + Nillable | field.UUID("token", uuid.UUID{}).Optional().Nillable() | *uuid.UUID | SetToken / SetNillableToken / ClearToken |
| published_at: пост ещё не вышел | Optional + Nillable | field.Time("published_at").Optional().Nillable() | *time.Time | SetPublishedAt / SetNillablePublishedAt / ClearPublishedAt |
| edits: счётчик правок поста | Optional | field.Int("edits").Optional() | int | SetEdits / AddEdits / ClearEdits |
| age: обязателен и положителен | обязательное поле | field.Int("age").Positive() | int | SetAge |

> **Рабочее правило.** Новые nullable-поля по умолчанию делайте Optional().Nillable(): nil-проверка стоит дешевле, чем отладка истории, где «не рассказал о себе» и «стёр био» неотличимы от пустой строки. Optional без Nillable оставляйте полям, у которых нулевое значение честно совпадает со смыслом «не указано»: сайт, счётчик, логин. А если пусто не должно быть никогда — Default, как у role. Указатели захламляют код? Иногда. Но это чище, чем второй, тайный смысл у пустой строки.

### Проверка: что не сгенерируется у role

**Вопрос:** role объявлен как field.Enum("role").Default("user") — без Optional. Какого метода не будет ни у одного билдера role?

- SetRole
- ✅ SetNillableRole
- ResetRole
- Все три метода будут

Обязательное поле — а Default обязательности не отменяет — получает только SetRole и ResetRole. Колонка NOT NULL, чистить нечего, поэтому ни ClearRole, ни SetNillableRole не генерируются: они существуют только у Optional-полей. ResetRole при этом не про базу — он отменяет незавершённое изменение внутри мутации.

Пустота разобрана, и схема User больше не полумера: шесть полей, каждое со своей стратегией — bio с указателем, website с нулевым значением, role с дефолтом, created_at с Immutable. Следующие два урока — рёбра: в пятом свяжем User и Post через O2M и M2O, в шестом добавим комментарии и many2many, и таблицы заговорят друг с другом. Тема пустоты при этом не закрывается, а переезжает: у будущих сущностей появятся свои Optional-поля вроде edited_at, и стратегии этого урока пригодятся там напрямую. Полный план из 25 уроков лежит на [странице курса ent](/courses/ent/), а домашка ниже закрепляет все три стратегии на живой схеме.

### Карточки: NULL-семантика ent

- **Optional vs Nillable** — Optional: колонка nullable, поле можно не задавать при создании, тип в структуре остаётся T — NULL читается как нулевое значение. Nillable: меняет тип на *T — NULL читается как nil. Пара Optional().Nillable() даёт честную пустоту: nil означает ровно «в базе NULL».
- **SetNillableBio** — Метод для Optional-полей, принимает *string: вызывает SetBio только если v != nil, при nil — no-op. При создании незаданное Optional-поле станет NULL, при обновлении значение не изменится. Генерируется даже без Nillable.
- **ClearBio** — Метод update-билдера: помечает поле на очистку, Save запишет NULL. В create-билдере его нет — «не задал» и так даёт NULL. Для обязательных полей (включая поля с Default) не генерируется: колонка NOT NULL.
- **BioIsNil и BioNotNil** — Предикаты для Optional-полей: разворачиваются в IS NULL и IS NOT NULL. В SQL сравнение с NULL через = и <> всегда даёт UNKNOWN, поэтому пустые записи ищутся только IsNil. Работают на уровне колонки — не зависят от стратегии поля.
- **Нулевое значение vs NULL** — string — "", int — 0, bool — false, time.Time — 0001-01-01. Optional без Nillable превращает NULL в нулевое значение при чтении; SetBio("") записывает пустую строку вместо NULL. Это разные состояния колонки, и предикат IsNil их не путает — путает человек.
- **Unique + Optional** — Легальная пара: несколько NULL в уникальном индексе PostgreSQL, SQLite и MySQL не конфликтуют — NULL не равен ничему. Смысл: значение уникально, если есть. Контраст: Default с Unique запрещён (прошлый урок).

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

- NULL в SQL — «значения нет»; у Go вместо этого нулевые значения: "", 0, false, нулевое время. ent переводит NULL в nil только у Nillable-полей, у остальных — в нулевое значение типа.
- Три стратегии пустоты: Optional+Nillable даёт *T и nil-проверку; Optional без Nillable — T, где нулевое значение совпадает со смыслом; Default вместо Optional — колонка NOT NULL, пустоты не бывает.
- Для каждого Optional-поля ent генерирует SetBio, SetNillableBio (принимает *string, no-op при nil) и ResetBio; ClearBio живёт только в update-билдерах — при создании «не задал» уже означает NULL.
- Обязательным полям, включая поля с Default, SetNillable и Clear не генерируются: колонка NOT NULL, чистить нечего.
- SetBio("") записывает пустую строку, а не NULL: предикат BioIsNil его не найдёт. Очистка существующего значения — только ClearBio().
- Поиск пустых записей — предикаты IsNil и NotNil (IS NULL и IS NOT NULL): сравнение с NULL через = и <> в SQL всегда даёт UNKNOWN.
- Unique сочетается с Optional: несколько NULL в уникальном индексе не конфликтуют; сочетание Unique с Default запрещено.

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

Добавьте в user.go поле website: field.String("website").Optional() — без Nillable — и запустите go generate ./ent. Создайте пользователя без сайта и без био: распечатайте u.Website и u.Bio и объясните себе разницу между "" и nil. Затем сравните два «стирания»: SetWebsite("") и ClearWebsite() — и проверьте через user.WebsiteIsNil(), кого из двух нашёл фильтр. Вопрос на подумать: почему для website мы не стали платить указателем, а для bio заплатили? Свой ответ сверьте с таблицей решений из урока.

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

### Чем Nillable лучше sql.NullString?

Это не «лучше и хуже», а разные школы. database/sql оборачивает значение в структуру с флагом Valid — так работает sqlc и сырой sql.Rows. ent генерирует указатели *T: nil проверяется обычным сравнением, а не .Valid, и значение естественно сериализуется в JSON. Внутри ent всё равно сканирует через sql.Null* — но наружу отдаёт указатели.

### SetNillableBio(nil) очистит поле при обновлении?

Нет. SetNillable-метод при nil — no-op: сгенерированный код не вызывает SetBio, и мутация уходит без изменения bio. Чтобы записать NULL в существующую строку, нужен ClearBio(). При создании разницы нет: незаданное Optional-поле и так станет NULL.

### Что выбрать для чисел и bool?

Если 0 и false могут быть настоящими значениями, а «не указано» надо различать — Optional().Nillable() с *int или *bool. Если нулевое значение и «нет данных» для вас одно и то же — хватит Optional без Nillable: меньше разыменований. Для полей с осмысленным значением по умолчанию — Default, как у role в микроблоге.
