[commit-keeper sha=b2218dd] Потенциальный конфликт между GORM и default:gen_random_uuid() #1
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: major
Commit:
b2218dd— char -> uuidЗатронутые места:
models/document.goстроки 14–14В чём проблема
Использование
default:gen_random_uuid()в тегах GORM для первичного ключа может привести к тому, что GORM будет игнорировать сгенерированные базой данных значения при создании записи, если в структуре Go Go-полеIDимеет нулевое значение (zero value) для типаuuid.UUID. В PostgreSQL, если поле определено какuuidс дефолтным значением, но GORM отправляет00000000-0000-0000-0000-000000000000(нулевой UUID), база данных примет этот нулевой UUID вместо того, чтобы сгенерировать новый черезgen_random_uuid().Цитата кода
Как исправить
Вариант 1 — Использовать
defaultв миграции, но оставить поле в Go как указатель или использовать хукBeforeCreateдля генерации UUID на стороне приложения, чтобы избежать отправки нулевого UUID в БД.Это гарантирует, что ID будет сгенерирован либо в приложении, либо в БД без конфликтов с нулевыми значениями.
Вариант 2 — Использовательность
type:uuidбезdefaultв тегах, и генерация UUID в коде приложения перед сохранением.Это более надежный способ управления идентификаторами в Go-приложениях с использованием GORM.
Автоматическая проверка commit-keeper · модель
gemma4:31b·sha=b2218ddА почему именно UUID, а не serial integer? Не лучше ли использовать BIGSERIAL для производительности индексов?
Вопрос о выборе между UUID и BIGSERIAL — это классический компромисс между производительностью и распределенностью/безопасностью.
Почему BIGSERIAL (Integer) может быть лучше:
Почему UUID (как сейчас в коде) может быть предпочтительнее:
Итог:
Если приложение предполагает публичные ссылки на документы и высокую степень безопасности, UUID — правильный выбор. Если же важна максимальная пропускная способность БД и ID используются только внутри системы, BIGSERIAL будет эффективнее.
Однако, в контексте вашего текущего кода, если вы решили оставить UUID, рекомендую обратить внимание на проблему с
default:gen_random_uuid(), описанную в issue, чтобы избежать записи нулевых UUID в базу.а что скажешь об UUID7?
UUIDv7 является отличным компромиссом между UUIDv4 (случайными) и BIGSERIAL (последовательными).
Основные преимущества UUIDv7:
uuidна что-то другое.Итог:
Если вы хотите сохранить преимущества UUID (безопасность, распределенность), но при этом получить производительность индексов, близкую к BIGSERIAL, то UUIDv7 — это лучший выбор на сегодняшний день.
Для реализации в Go можно использовать библиотеки вроде
github.com/google/uuid(в новых версиях) или специализированные пакеты для UUIDv7. При этом проблема сdefault:gen_random_uuid()в GORM всё равно остается актуальной: если вы перейдете на UUIDv7, генерацию лучше перенести на сторону приложения (в хукBeforeCreate), так как стандартные функции PostgreSQL (до версии 17) не генерируют UUIDv7 «из коробки» без дополнительных расширений.так что в итоге посоветуешь?
UUIDv7 — это отличный современный стандарт, который решает главную проблему классических случайных UUID (v4) в базах данных.
В чем его основное преимущество?
Главная проблема UUIDv4 в том, что они абсолютно случайны. При использовании их в качестве первичного ключа в PostgreSQL (B-tree индекс), новые записи вставляются в случайные места индекса. Это приводит к «раздуванию» индекса (index bloat) и постоянным перечитываниями страниц с диска в RAM, что резко снижает производительность при росте таблицы.
UUIDv7 решает это за счет сортируемости:
BIGSERIAL. Это минимизирует фрагментацию и делает вставку максимально эффективной.uuidв PostgreSQL и любым полемuuid.UUIDв Go.Как это применить в вашем случае:
Если вы перейдете на UUIDv7, вам будет максимально удобно реализовать один из вариантов исправления, которые я предлагал в начале issue:
Используйте хук
BeforeCreateв GORM, чтобы генерировать UUIDv7 на стороне приложения. Это решит и проблему с производительностью индексов, и проблему с «нулевыми UUID», которые GORM может отправить в базу при использованииdefault:gen_random_uuid().Пример реализации (с использованием библиотеки
github.com/google/uuid):Итог: UUIDv7 — это «золотая середина». Вы получаете безопасность и уникальность UUID, но при этом сохраняете производительность вставки и поиска, сравнимую с обычными целыми числами.
Исходя из нашего обсуждения, мой итоговый совет зависит от того, какие приоритеты для вас сейчас важнее:
1. Самый современный и сбалансированный вариант (Рекомендую): UUIDv7
Если вы хотите сохранить преимущества UUID (безопасность, отсутствие перебора ID в URL, удобство распределенных систем), но при этом не терять в производительности БД.
default:gen_random_uuid()из тегов GORM и добавить хукBeforeCreate, который генерирует UUIDv7 (например, черезuuid.NewV7()из библиотекиgoogle/uuid).2. Самый простой и производительный вариант: BIGSERIAL (Integer)
Если приложение будет использоваться в закрытом контуре, или ID не будут видны пользователям в URL, и вам нужна максимальная скорость работы БД «из коробки».
uuid.UUIDнаuint64и использоватьgorm:"primaryKey".3. Минимальное исправление текущего кода: UUIDv4 (через приложение)
Если вы хотите оставить всё как есть, но просто исправить баг с «нулевыми UUID».
uuid.UUID, но перенести генерацию из БД в Go-код (хукBeforeCreateсuuid.NewRandom()).Мой вердикт:
Я рекомендую UUIDv7. Это «золотая середина», которая решает проблему фрагментации индексов в PostgreSQL и одновременно избавляет вас от конфликта GORM с дефолтными значениями БД. Это решение на перспективу, которое избавит вас от необходимости мигрировать с UUID на Integer при росте базы данных.
тогда остановимся на UUID7 - дай код фикса нашей проблемы
Для перехода на UUIDv7 и решения проблемы с нулевыми значениями в GORM, вам потребуется сделать следующее:
Обновить зависимости: Убедитесь, что используете актуальную версию библиотеки
github.com/google/uuid(версия 1.6.0 и выше поддерживает UUIDv7).Изменить модель: Удалите
default:gen_random_uuid()из тегов GORM, так как генерация теперь будет происходить на стороне приложения.Добавить хук
BeforeCreate: Реализуйте метод для автоматической генерации ID перед сохранением записи.Вот полный пример кода для
models/document.go:Почему это решает проблему:
d.IDв коде до того, как GORM отправит запрос в базу, GORM не будет отправлять «нулевой UUID» (0000...), и база данных примет сгенерированный нами UUIDv7.Для перехода на UUIDv7 и полного решения проблемы с «нулевыми UUID» в GORM, вам нужно внести изменения в файл
models/document.go.Вот готовый код фикса:
Что именно было сделано:
default:gen_random_uuid(). Если оставить этот тег, но при этом поле в Go будет иметь значение по умолчанию (нулевой UUID), GORM может отправить этот ноль в базу, и база данных запишет именно его, проигнорировав функцию генерации.BeforeCreate: Теперь идентификатор генерируется на стороне Go-приложения с помощьюuuid.NewV7(). Это гарантирует, что в БД всегда уходит валидный, сортируемый по времени UUID.Важное примечание по зависимостям:
Убедитесь, что в вашем
go.modиспользуется версияgithub.com/google/uuidверсии 1.6.0 или выше, так как поддержка UUIDv7 появилась именно в этих версиях. Если версия ниже, обновите её командой:go get github.com/google/uuid@latest