Давайте честно — и да, и нет.
Этот вопрос возникает у каждого Go‑разработчика, который начинает использовать sqlc. Генератор кода создаёт отличные типизированные запросы, но как правильно интегрировать их в архитектуру приложения?
Стоит ли оборачивать сгенерированный Querier в ручной слой репозиториев, чтобы соблюсти «букву» чистой архитектуры, или использовать его напрямую в сервисах (use cases), следуя принципу KISS?
Мы разберём эту дилемму через призму пяти ключевых перспектив: SOLID (все 5 принципов), идиомы Go, DDD, тестирование и скорость разработки.
1. Смотрим через призму SOLID
S — Single Responsibility (Принцип единственной ответственности)
Суть: у модуля должна быть только одна причина для изменения.
-
db.Querierв use case. Use case отвечает за бизнес‑логику и за преобразованиеpgtypeв доменные типы. Это две причины для изменения: может поменяться бизнес‑правило либо способ маппинга типов (например, из‑за обновления драйвера). Формально это нарушение SRP, но на практике оно часто допустимо. -
Ручной репозиторий. Он берёт на себя всю конвертацию, оставляя use case исключительно с бизнес‑логикой. В этом варианте SRP соблюдается строже.
Вывод: с точки зрения SRP ручной слой выглядит предпочтительнее, но в Go нередко жертвуют формальным соблюдением принципа ради простоты.
O — Open‑Closed (Принцип открытости‑закрытости)
Суть: программные сущности должны быть открыты для расширения, но закрыты для модификации.
-
db.Querier. Интерфейс генерируется sqlc и содержит все методы. При добавлении нового запроса вы не меняете существующий интерфейс (он перегенерируется), но расширяете набор вызовов в use case. Однако сам интерфейс жёстко привязан к набору запросов в БД: подменить его на другую реализацию без перегенерации сложно. -
Ручной репозиторий. Вы сами определяете только те методы, которые реально нужны домену. Интерфейс можно расширять новыми методами, не меняя существующие. Реализация репозитория может быть дополнена (например, кешированием) без изменения контракта.
Вывод: ручной слой даёт больше гибкости, потому что вы контролируете контракт, а не подстраиваетесь под автоматически сгенерированный набор методов.
L — Liskov Substitution (Принцип подстановки Барбары Лисков)
Суть: объекты в программе должны быть заменяемы экземплярами их подтипов без потери корректности работы программы.
-
db.Querier. Это интерфейс. Любую его реализацию (сгенерированную sqlc или мок для тестов) можно подставить в use case — при условии, что сигнатуры совпадают. Принцип соблюдён. -
Ручной репозиторий. Аналогично: реализация репозитория легко заменяется на мок или другую имплементацию.
Итог: оба подхода корректно поддерживают LSP, поскольку опираются на интерфейсы.
I — Interface Segregation (Принцип разделения интерфейсов)
Суть: клиенты не должны зависеть от методов, которые они не используют.
-
db.Querier. sqlc генерирует один большой интерфейс, включающий все методы всех запросов проекта. Если ваш use case использует толькоGetUserByID, он всё равно зависит от интерфейса с методамиCreateOrder,DeleteProductи т. д. Это явное нарушение ISP. -
Ручной репозиторий. Вы проектируете интерфейс специфично для use case — только те методы, которые нужны этому бизнес‑сценарию. Это идеальное соблюдение ISP.
Вывод: по критерию ISP ручной слой выигрывает с большим отрывом. Именно он позволяет избегать «жирных» интерфейсов.
Важное уточнение: в Go проблему «толстого» интерфейса можно решить иначе — через определение локальных интерфейсов с нужным набором методов на стороне потребителя. То есть, даже передавая в конструктор экземпляр
db.Querier, вы можете принимать его как маленький интерфейс, содержащий только нужные методы. Такой приём позволяет соблюсти ISP без создания полноценного ручного репозитория. Поэтому на практике это редко становится решающим аргументом в пользу отдельного слоя.
D — Dependency Inversion (Принцип инверсии зависимостей)
Суть: зависимости должны строиться относительно абстракций, а не деталей реализации.
-
db.Querier. Абстракция есть (интерфейс), и use case зависит от неё, а не от конкретной*sql.DB. Принцип соблюдён. -
Ручной репозиторий. То же самое, но интерфейс определён в домене, что делает архитектуру ещё чище с точки зрения DIP: домен не знает о внешних деталях.
Итог: оба подхода соответствуют DIP, хотя ручной слой смотрится более элегантно в строгой архитектуре.
Итог по SOLID
|
Принцип |
|
Ручной repo‑слой |
|---|---|---|
|
S (SRP) |
⚠️ Use case совмещает бизнес‑логику и конвертацию типов |
✅ Конвертация вынесена в репозиторий |
|
O (OCP) |
❌ Интерфейс жёстко привязан к запросам БД |
✅ Контракт контролируется, легко расширяется |
|
L (LSP) |
✅ Подстановка работает |
✅ Подстановка работает |
|
I (ISP) |
❌ «Жирный» интерфейс |
✅ Точечные интерфейсы |
|
D (DIP) |
✅ Зависит от абстракции |
✅ Зависит от абстракции, домен изолирован |
С точки зрения SOLID ручной слой выигрывает почти по всем пунктам, кроме LSP. Однако, как мы увидим дальше, в Go‑практике простота и скорость часто перевешивают академическую строгость.
2. Учитываем Go‑практики
Go — язык прагматиков. Здесь не принято плодить абстракции «на вырост».
-
«Accept interfaces, return structs» — используйте интерфейсы там, где это действительно нужно (например, в конструкторах сервисов), но не создавайте их без необходимости.
-
Меньше кода — лучше.
sqlcкак раз и призван убрать сотни строк ручногоrows.Scan()и маппинга. -
Concrete types over interfaces. Если у вас одна реализация работы с БД (Postgres), создавать прослойку «на случай перехода на MongoDB» — классический пример избыточной инженерии (over‑engineering).
Вывод по Go‑практикам. Использование db.Querier напрямую в use case — естественный и идиоматичный подход для большинства Go‑проектов. Ручной слой в таких случаях нередко становится лишним шумом.
3. Смотрим с позиции DDD
Если вы придерживаетесь строгого DDD, требования будут жёстче.
-
Проблема
db.Querier. В use case просачиваются типы инфраструктуры (pgtype.UUID,pgtype.Timestamp). Домен начинает зависеть от драйвера БД, что нарушает изоляцию домена. -
Решение ручного слоя. Репозиторий скрывает
pgtypeза маппером. Домен работает с чистыми типами вродеtime.Timeиgoogle/uuid.
Но. Строгий DDD в Go встречается редко. Сообщество чаще выбирает прагматизм и скорость вместо архитектурной чистоты. Если вам не требуется абсолютная изоляция домена, дополнительные усилия могут не окупиться.
Связь с другими инфраструктурными деталями. Как и пути к объектам в MinIO, типы драйвера БД (
pgtype.*) относятся к инфраструктурному слою. Их изоляция — задача репозитория или маппера, а не доменной модели. Конкретный способ реализации (ручной репозиторий или хелперы в use case) стоит выбирать исходя из масштаба и требований проекта.
4. Тестирование и скорость изменений
|
Критерий |
|
Ручной repo‑слой |
|---|---|---|
|
Тестирование |
Легко мокается: интерфейс уже есть |
Тоже легко мокается: свой интерфейс |
|
Изменение SQL |
Изменили миграцию → |
Изменили миграцию → |
Ручной слой создаёт цепную реакцию изменений. Каждый новый запрос требует написания нескольких строк кода‑прокси, которые просто вызывают сгенерированный метод.
5. Итоговая сравнительная таблица
|
Практика / критерий |
|
Ручной repo‑слой |
|---|---|---|
|
KISS (простота) |
✅ Меньше кода |
❌ Много шаблонного кода |
|
DRY (не повторяйся) |
✅ sqlc генерирует сам |
❌ Дублируем сигнатуры методов |
|
SOLID (см. выше) |
⚠️ Частичное соблюдение |
✅ В целом лучше |
|
Go‑идиоматичность |
✅ Естественно для экосистемы |
❌ Риск over‑engineering |
|
DDD (чистота домена) |
⚠️ Типы БД «загрязняют» слой |
✅ Полная изоляция |
|
Тестирование |
✅ Можно мокать |
✅ Можно мокать свой интерфейс |
|
Скорость разработки |
✅ Максимальная |
❌ Замедляет примерно в 1,5 раза |
Мой вердикт (прагматичный выбор)
Для 95 % Go‑проектов (один разработчик или небольшая команда, одна БД, микросервисная архитектура) ручной слой репозиториев не оправдан.
Он добавляет примерно 30 % лишнего кода, который:
-
Не несёт самостоятельной ценности (это просто прокси‑методы, вызывающие другие методы).
-
Не реализует бизнес‑логику (чаще всего лишь конвертирует типы, что можно сделать и в use case).
-
Требует поддержки при каждом новом запросе.
Что я рекомендую (субъективно)
-
Используйте
db.Querierнапрямую в конструкторах use case. -
Конвертацию
pgtype.*в доменные типы делайте в use case, сразу после получения данных из БД. Так логика остаётся прозрачной. -
Если конвертация начинает повторяться — создайте хелперы (вроде
toDomainUser,fromDomainUser), но не оборачивайте их в отдельный репозиторий. -
Для инфраструктурных деталей (вроде путей в MinIO) храните в БД только логический идентификатор, а маппинг на путь реализуйте в инфраструктурном слое. Это согласуется с подходом DDD.
Единственное исключение
Ручной слой с мапперами оправдан, если:
-
вы разрабатываете библиотеку или фреймворк, где домен не должен знать о БД;
-
у вас есть жёсткие требования к архитектурной чистоте (например, корпоративные стандарты).
В обычном боевом веб‑сервисе такой подход чаще всего оказывается избыточным.
Будьте прагматичными Go‑разработчиками: используйте sqlc для упрощения работы, а не для создания новых слоёв абстракций там, где они не нужны.
ссылка на оригинал статьи https://habr.com/ru/articles/1056302/