Какое ORM использовать в GO: GORM, sqlc, Ent или Bun?

GORM против sqlc против Ent против Bun

Содержимое страницы

Экосистема Go предлагает широкий выбор инструментов ORM (Object-Relational Mapping) и библиотек баз данных, каждая из которых имеет свою собственную философию. Вот всестороннее сравнение четырех основных решений для использования PostgreSQL в Go: GORM, sqlc, Ent, и Bun.

Давайте оценим их по производительности, удобству разработки, популярности и функциям/расширяемости, с примерами кода, демонстрирующими базовые операции CRUD на модели User. Промежуточные и опытные разработчики Go получат представление о компромиссах каждого инструмента и о том, какой из них может лучше всего соответствовать их потребностям.

экран ноутбука с кодом

Обзор ORM

  • GORM – это мощный ORM в стиле Active Record. GORM позволяет определять структуры Go в качестве моделей и предоставляет обширный API для запросов и манипуляции данными с помощью цепочки методов. Он существует уже много лет и является одним из самых широко используемых ORM для Go (почти 39k звезд на GitHub). Привлекательность GORM заключается в его мощном наборе функций (автоматические миграции, обработка отношений, жадная загрузка, транзакции, хуки и многое другое), что позволяет создавать чистый код Go с минимальным количеством сырых SQL-запросов. Однако он вводит свои собственные паттерны и имеет накладные расходы на выполнение из-за рефлексии и использования интерфейсов, что может влиять на производительность при больших операциях.

  • sqlc – это не традиционный ORM, а генератор SQL-кода. С sqlc вы пишете обычные SQL-запросы (в файлах .sql), и инструмент генерирует безопасные для типов Go-код (функции DAO) для выполнения этих запросов. Это означает, что вы взаимодействуете с базой данных, вызывая сгенерированные функции Go (например, CreateUser, GetUser) и получая строго типизированные результаты без ручного сканирования строк. Sqlc фактически позволяет использовать сырые SQL-запросы с проверкой на этапе компиляции — нет слоя построения запросов или рефлексии на этапе выполнения. Он быстро стал популярным (~16k звезд) благодаря своей простоте и производительности: он полностью избегает накладных расходов на этапе выполнения, используя подготовленные SQL-запросы, что делает его таким же быстрым, как использование database/sql напрямую. Компромисс заключается в том, что вам нужно писать (и поддерживать) SQL-выражения для ваших запросов, и динамическая логика запросов может быть менее очевидной по сравнению с ORM, которые строят SQL для вас.

  • Ent (Entgo) – это ORM с кодом первой, который использует генерацию кода для создания безопасного для типов API для вашей модели данных. Вы определяете свою схему в Go (используя DSL Ent для полей, связей/отношений, ограничений и т.д.), затем Ent генерирует Go-пакеты для моделей и методов запросов. Сгенерированный код использует флюентный API для построения запросов с полной проверкой типов на этапе компиляции. Ent относительно новый (поддерживается Linux Foundation и разработан Ariga). Он делает упор на удобство разработки и корректность — запросы строятся через цепочку методов вместо сырых строк, что делает их легче для композиции и менее подверженными ошибкам. Подход Ent приводит к высокооптимизированным SQL-запросам и даже кэширует результаты запросов для уменьшения дублирующихся обращений к базе данных. Он был разработан с учетом масштабируемости, поэтому эффективно обрабатывает сложные схемы и отношения. Однако использование Ent добавляет шаг сборки (запуск codegen) и привязывает вас к его экосистеме. В настоящее время он поддерживает PostgreSQL, MySQL, SQLite (и экспериментальную поддержку других баз данных), тогда как GORM поддерживает более широкий диапазон баз данных (включая SQL Server и ClickHouse).

  • Bun – это новый ORM с “SQL-first” подходом. Bun вдохновлен Go database/sql и старой библиотекой go-pg, стремясь быть легковесным и быстрым с акцентом на функции PostgreSQL. Вместо паттерна Active Record Bun предоставляет флюентный строитель запросов: вы начинаете запрос с db.NewSelect() / NewInsert() / и т.д., и строите его с методами, которые тесно напоминают SQL (вы даже можете встраивать сырые SQL-фрагменты). Он отображает результаты запросов на структуры Go с помощью тегов структур, похожих на теги GORM. Bun предпочитает явность и позволяет легко переходить к сырому SQL при необходимости. Он не запускает миграции автоматически (вы пишете миграции или используете пакет migrate Bun вручную), что некоторые считают плюсом для контроля. Bun хвалят за элегантное обращение с сложными запросами и поддержку продвинутых типов Postgres (массивы, JSON и т.д.) из коробки. На практике Bun накладывает гораздо меньше накладных расходов на выполнение, чем GORM (нет тяжелой рефлексии на каждом запросе), что приводит к лучшей пропускной способности в сценариях высокой нагрузки. Его сообщество меньше (~4k звезд), и его набор функций, хотя и солидный, не такой обширный, как у GORM. Bun может потребовать немного больше знаний SQL для эффективного использования, но он вознаграждает это производительностью и гибкостью.

Бенчмарки производительности

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

  • GORM: Удобство GORM обходится некоторой производительностью. Для простых операций или небольших наборов результатов GORM может быть довольно быстрым — на самом деле, один бенчмарк показал, что GORM имел самое быстрое время выполнения для очень маленьких запросов (например, получение 1 или 10 записей). Однако по мере увеличения количества записей накладные расходы GORM вызывают его значительное отставание. В тесте получения 15 000 строк GORM был примерно в 2 раза медленнее, чем sqlc или даже сырый database/sql (59,3 мс для GORM против ~31,7 мс для sqlc и 32,0 мс для database/sql в этом сценарии). Дизайн, основанный на рефлексии, и абстракции построения запросов добавляют задержку, и GORM может выдавать несколько запросов для сложных загрузок (например, один запрос на каждую связанную сущность при использовании Preload по умолчанию), что может усилить задержку. Вкратце: GORM обычно подходит для умеренных нагрузок, но в сценариях высокой пропускной способности или больших пакетных операциях его накладные расходы могут стать узким местом.

  • sqlc: Поскольку вызовы sqlc по сути являются рукописными SQL-запросами под капотом, его производительность сопоставима с использованием стандартной библиотеки напрямую. Бенчмарки постоянно помещают sqlc среди самых быстрых вариантов, часто лишь незначительно уступая или даже немного превосходя сырый database/sql для сопоставимых операций. Например, при 10 000+ записях sqlc показал небольшое превосходство над обычным database/sql в пропускной способности (вероятно, из-за избежания некоторых шаблонов сканирования и использования эффективной генерации кода). С sqlc нет построителя запросов на этапе выполнения — ваш запрос выполняется как подготовленное выражение с минимальными накладными расходами. Суть в том, что вы уже сделали работу по написанию оптимального SQL-запроса; sqlc просто избавляет вас от усилий по сканированию строк в типы Go. На практике это означает отличную масштабируемость — sqlc будет обрабатывать большие объемы данных так же хорошо, как и сырые SQL-запросы, делая его лучшим выбором, когда критична скорость.

  • Ent: Производительность Ent находится где-то между подходами с сырым SQL и традиционными ORM. Поскольку Ent генерирует безопасный для типов код, он избегает рефлексии и большинства затрат на построение запросов на этапе выполнения. SQL, который он производит, довольно оптимизирован (у вас есть контроль для написания эффективных запросов через API Ent), и Ent включает внутренний кэш для повторного использования планов выполнения/результатов запросов в некоторых случаях. Это может уменьшить повторяющиеся обращения к базе данных в сложных рабочих процессах. Хотя конкретные бенчмарки Ent по сравнению с другими варьируются, многие разработчики сообщают, что Ent превосходит GORM для эквивалентных операций, особенно по мере увеличения сложности. Одна из причин заключается в том, что GORM может генерировать неоптимальные запросы (например, N+1 выборки, если не осторожно использовать соединения/предзагрузки), тогда как Ent поощряет явную жадную загрузку отношений и будет объединять данные в меньшем количестве запросов. Ent также склонен выделять меньше памяти на операцию, чем GORM, что может улучшить пропускную способность, снижая нагрузку на сборщик мусора Go. В целом, Ent разработан для высокой производительности и больших схем — его накладные расходы невелики, и он может эффективно обрабатывать сложные запросы — но он может не соответствовать пропускной способности рукописных SQL (sqlc) во всех сценариях. Это сильный конкурент, если вы хотите и скорость, и безопасность слоя ORM.

  • Bun: Bun был создан с учетом производительности и часто упоминается как более быстрая альтернатива GORM. Он использует флюентный API для построения SQL-запросов, но эти строители легковесны. Bun не скрывает SQL от вас — это скорее тонкий слой поверх database/sql, что означает, что вы несете очень мало накладных расходов сверх того, что делает стандартная библиотека Go. Пользователи отмечали значительные улучшения при переходе от GORM к Bun в крупных проектах: например, один отчет упоминал, что GORM был “очень медленным” для их масштаба, и замена его на Bun (и некоторый сырой SQL) решила их проблемы с производительностью. В бенчмарках, таких как go-orm-benchmarks, Bun обычно находится в верхней части списка по скорости для большинства операций (часто в пределах 1,5× от sqlx/sql) и значительно опережает GORM по пропускной способности. Он также поддерживает пакетные вставки, ручное управление соединениями против отдельных запросов и другие оптимизации, которые разработчики могут использовать. Итог: Bun обеспечивает производительность, близкую к “голому металлу”, и это отличный выбор, когда вы хотите удобство ORM, но не можете пожертвовать скоростью. Его SQL-first природа гарантирует, что вы всегда можете оптимизировать запросы, если сгенерированные не оптимальны.

Итог по производительности: Если главная цель — максимальная производительность (например, приложения, интенсивно работающие с данными, микросервисы под высокой нагрузкой), sqlc или даже рукописные вызовы database/sql являются победителями. Они практически не имеют абстракционного наклада и масштабируются линейно. Ent и Bun также работают очень хорошо и могут эффективно обрабатывать сложные запросы — они находят баланс между скоростью и абстракцией. GORM предлагает наибольшее количество функций, но за счет накладных расходов; он вполне приемлем для многих приложений, но вы должны быть осведомлены о его влиянии, если ожидаете обрабатывать огромные объемы данных или нуждаетесь в ультранизкой задержке. (В таких случаях вы можете снизить затраты GORM, тщательно используя Joins вместо Preload для уменьшения количества запросов, или добавляя сырые SQL для критических путей, но это добавляет сложности.)

Опыт разработчика и удобство использования

Опыт разработчика может быть субъективным, но он включает в себя кривую обучения, понятность API и то, насколько продуктивным вы можете быть в повседневной работе с кодом. Вот как наши четыре конкурента сравниваются в плане удобства использования и рабочего процесса разработки:

  • GORM – Многофункциональный, но с кривой обучения: GORM часто является первым ORM, который пробуют новички в Go, потому что он обещает полностью автоматизированный опыт (вы определяете структуры и сразу можете Create/Find без написания SQL). Документация очень подробная с множеством руководств, что помогает. Однако у GORM есть свой собственный идиоматический способ работы (“кодовый подход” к взаимодействию с БД), и это может показаться неудобным вначале, если вы привыкли к “сырому” SQL. Многие распространенные операции просты (например, db.Find(&objs)), но когда вы переходите к ассоциациям, полиморфным отношениям или сложным запросам, вам нужно изучить соглашения GORM (теги структур, Preload vs Joins и т.д.). Поэтому часто упоминают о крутой начальной кривой обучения. Как только вы преодолеете этот этап, GORM может быть очень продуктивным — вы тратите меньше времени на написание повторяющегося SQL и больше времени на логику Go. Фактически, после освоения многие находят, что могут разрабатывать функции быстрее с GORM, чем с библиотеками более низкого уровня. Короче говоря, опыт разработчика с GORM: высокая начальная сложность, но становится плавнее с опытом. Большое сообщество означает, что доступно много примеров и ответов на StackOverflow, а богатая экосистема плагинов может еще больше упростить задачи (например, плагины для мягкого удаления, аудита и т.д.). Основное предупреждение заключается в том, что вы должны оставаться в курсе того, что делает GORM под капотом, чтобы избежать ловушек (например, случайных N+1 запросов). Но для разработчика, который инвестирует время в GORM, он действительно “чувствуется” как мощное, интегрированное решение, которое многое делает за вас.

  • Ent – Схема-первый и безопасный по типам: Ent был специально разработан для улучшения опыта разработчика ORM. Вы описываете свою схему в одном месте (код на Go) и получаете строго типизированный API для работы — это означает отсутствие строковых запросов, и многие ошибки обнаруживаются на этапе компиляции. Например, если вы попытаетесь обратиться к полю или связи, которого не существует, ваш код просто не скомпилируется. Эта безопасная сеть — огромное преимущество для больших проектов. API Ent для построения запросов плавный и интуитивно понятный: вы цепочкой вызываете методы, такие как .Where(user.EmailEQ("alice@example.com")), и это читается почти как английский. Разработчики, пришедшие из ORM других языков, часто находят подход Ent естественным (он somewhat подобен Entity Framework или Prisma, но на Go). Изучение Ent требует понимания его рабочего процесса генерации кода. Вам нужно будет запускать entc generate (или использовать go generate) каждый раз, когда вы изменяете схему. Это дополнительный шаг, но обычно часть процесса сборки. Кривая обучения для Ent умеренная — если вы знаете Go и основы SQL, концепции Ent (Поля, Связи и т.д.) просты. Фактически, многие находят Ent проще для понимания, чем GORM, потому что вам не нужно гадать, какой SQL генерируется; вы можете часто предсказать его по именам методов, и вы можете вывести запрос для отладки, если нужно. Документация и примеры Ent довольно хороши, а поддержка со стороны компании гарантирует его активное развитие. В целом опыт разработчика с Ent: очень дружелюбен для тех, кто хочет ясности и безопасности. Код, который вы пишете, более многословен по сравнению с “сырым” SQL, но он самодокументируемый. Также рефакторинг безопаснее (переименование поля в схеме и повторная генерация обновит все использования в запросах). Недостаток может заключаться в том, что вы somewhat ограничены тем, что предоставляет кодогенерация Ent — если вам нужен очень специфический запрос, вы можете либо написать его с помощью API Ent (который может обрабатывать сложные соединения, но иногда вы можете найти случай, который проще написать на “сыром” SQL). Ent позволяет вставлять фрагменты “сырого” SQL при необходимости, но если вы часто это делаете, вы можете задуматься, подходит ли вам ORM. В итоге, Ent предоставляет плавный опыт разработчика для большинства операций CRUD и логики запросов с минимальными сюрпризами, при условии, что вы согласны запускать шаг кодогенерации и придерживаться паттернов, которые навязывает Ent.

  • Bun – Ближе к SQL, меньше магии: Использование Bun отличается от использования GORM или Ent. Философия Bun — не скрывать SQL — если вы знаете SQL, вы уже знаете, как использовать Bun в значительной степени. Например, чтобы запросить пользователей, вы можете написать: db.NewSelect().Model(&users).Where("name = ?", name).Scan(ctx). Это плавный API, но очень близкий к структуре реального SELECT-запроса. Кривая обучения для Bun обычно низкая, если вы комфортно чувствуете себя с SQL. Меньше “магии” ORM нужно изучать; вы в основном учите имена методов для построения запросов и соглашения о тегах структур. Это делает Bun довольно доступным для опытных разработчиков, которые использовали database/sql или другие библиотек-построители запросов, такие как sqlx. В отличие от этого, новичок, который не уверен в написании SQL, может найти Bun менее полезным, чем GORM — Bun не будет автоматически генерировать сложные запросы для вас через отношения, если вы их не укажете. Однако Bun поддерживает отношения и жадную загрузку (метод Relation() для соединения таблиц) — он просто делает это явно, что нравится некоторым разработчикам за ясность. Опыт разработчика с Bun можно описать как легковесный и предсказуемый. Вы пишете немного больше кода, чем с GORM, для некоторых операций (поскольку Bun часто просит вас явно указывать столбцы или соединения), но взамен у вас больше контроля и видимости. Минимальная внутренняя магия, поэтому отладка проще (вы обычно можете записать строку запроса, которую построил Bun). Документация Bun (руководство uptrace.dev) подробная и включает паттерны для миграций, транзакций и т.д., хотя меньшее сообщество означает меньше сторонних учебных материалов. Еще один аспект DX — доступные инструменты: Bun, будучи просто расширением database/sql, означает, что любой инструмент, работающий с sql.DB (например, отладочные прокси, логи запросов), легко работает с Bun. В итоге, Bun предлагает опыт без лишних хлопот: это похоже на написание структурированного SQL на Go. Это отлично для разработчиков, которые ценят контроль и производительность, но это может не так сильно помогать менее опытным разработчикам, как что-то вроде GORM. Консенсус заключается в том, что Bun делает простые вещи легкими, а сложные — возможными (как и “сырой” SQL), не навязывая много фреймворка.

Популярность и поддержка экосистемы

Уровень принятия и поддержка сообщества могут повлиять на ваш выбор — популярная библиотека означает больше вкладов сообщества, лучшее обслуживание и больше ресурсов для изучения.

  • GORM: Будучи самой старой и зрелой из четырех, GORM имеет наибольшую пользовательскую базу и экосистему. В настоящее время это самый популярный ORM для Go на GitHub (более 38k звезд), и он используется в бесчисленном количестве производственных проектов. Разработчики активно поддерживают проект, и он регулярно обновляется (GORM v2 была значительной переработкой, улучшившей производительность и архитектуру). Большим преимуществом популярности GORM является богатство расширений и интеграций. Существуют официальные и сторонние плагины для таких вещей, как драйверы баз данных (например, для PostgreSQL, MySQL, SQLite, SQL Server, ClickHouse), готовые к использованию. GORM также поддерживает широкий набор сценариев использования: миграции, автоматическое создание схемы, мягкое удаление, JSON-поля (с gorm.io/datatypes), полнотекстовый поиск и т.д., часто через встроенные функции или дополнения. Сообщество создало различные инструменты, такие как gormt (для генерации определений структур на основе существующей базы данных), и множество учебных пособий и примеров. Если у вас возникнет проблема, быстрый поиск, скорее всего, найдет вопрос или вопрос на Stack Overflow, заданный кем-то другим. Итог по экосистеме: GORM чрезвычайно хорошо поддерживается. Это “стандартный” выбор ORM для многих, что означает, что вы найдете его в фреймворках и шаблонах. Обратная сторона заключается в том, что его большой размер может делать его тяжелым, но для многих это оправданная компромисс за глубину сообщества.

  • Ent: Несмотря на то, что он появился относительно недавно (открытый исходный код около 2019 года), Ent имеет сильное сообщество. С ~16k звездами и поддержкой Linux Foundation, это не маргинальный проект. Крупные компании начали использовать Ent благодаря его преимуществам, ориентированным на схему, а разработчики (из Ariga) предоставляют корпоративную поддержку, что является признаком уверенности в использовании для бизнес-критических задач. Экосистема вокруг Ent растет: существуют расширения Ent (ent/go) для таких вещей, как интеграция OpenAPI/GraphQL, gRPC и даже инструмент миграции SQL, работающий со схемами Ent. Поскольку Ent генерирует код, некоторые паттерны экосистемы отличаются — например, если вы хотите интегрировать GraphQL, вы можете использовать генерацию кода Ent для создания GraphQL-резолверов. Ресурсы для изучения Ent хорошие (официальная документация и пример проекта, охватывающий простое приложение). Форумы сообщества и обсуждения на GitHub активны с вопросами по проектированию схем и советами. В плане поддержки сообщества Ent, безусловно, вышел за рамки фазы “раннего принятия” и считается готовым к производству и надежным. У него может быть не так много ответов на Stack Overflow, как у GORM, но он быстро догоняет. Стоит отметить: поскольку Ent более категоричен (например, он хочет управлять схемой), вы, скорее всего, будете использовать его самостоятельно, а не вместе с другим ORM. У него нет “плагинов” в том же смысле, что и у GORM, но вы можете писать пользовательские шаблоны для расширения генерации кода или подключаться к событиям жизненного цикла (Ent поддерживает хуки/промежуточное ПО для сгенерированных клиентов). Поддержка со стороны фонда указывает на долгосрочную поддержку, поэтому выбор Ent — это безопасная ставка, если его модель соответствует вашим потребностям.

  • Bun: Bun (часть открытого пакета Uptrace) набирает популярность, особенно среди тех, кто был поклонником теперь не поддерживаемой библиотеки go-pg. С ~4.3k звездами, это самое маленькое сообщество в этом сравнении, но это очень активный проект. Разработчик оперативно отвечает и быстро добавляет новые функции. Сообщество Bun восторженно относится к его производительности. Вы найдете обсуждения на форумах Go и Reddit от разработчиков, которые перешли на Bun ради скорости. Однако из-за меньшей пользовательской базы вы не всегда сможете легко найти ответы на узкоспециализированные вопросы — иногда вам придется читать документацию/исходный код или задавать вопросы в GitHub или Discord (Uptrace предоставляет чат сообщества). Экосистема расширений более ограничена: Bun поставляется со своей библиотекой миграций, загрузчиком фикстур и интегрируется с инструментами наблюдения Uptrace, но вы не найдете такое множество плагинов, как у GORM. Тем не менее, Bun совместим с использованием sql.DB, поэтому вы можете комбинировать их — например, использовать github.com/jackc/pgx в качестве драйвера или интегрироваться с другими пакетами, ожидающими *sql.DB. Bun не заставляет вас использовать только его. В плане поддержки, будучи новым, документация актуальна, а примеры современны (часто показывают использование с контекстом и т.д.). Официальная документация сравнивает Bun с GORM и Ent напрямую, что полезно. Если размер сообщества вызывает опасения, одной стратегией может быть использование Bun по его преимуществам, но поддержание относительно поверхностного использования (например, вы можете заменить его на другое решение, если потребуется, поскольку он не навязывает тяжелую абстракцию). В любом случае, траектория Bun идет вверх, и он заполняет определенную нишу (ORM, ориентированный на производительность), что придает ему устойчивость.

  • sqlc: sqlc довольно популярен в сообществе Go, что подтверждается ~15.9k звездами и множеством сторонников, особенно в кругах, ориентированных на производительность. Его часто рекомендуют в обсуждениях об “избегании ORM”, поскольку он находит хороший баланс. Инструмент поддерживается участниками (включая оригинального автора, который активно его улучшает). Будучи больше компилятором, чем библиотекой времени выполнения, его экосистема вращается вокруг интеграций: например, редакторы/IDE могут иметь подсветку синтаксиса для файлов .sql, и вы запускаете sqlc generate как часть вашего сборки или CI-процесса. Сообщество создало шаблоны и базовые примеры репозиториев о том, как организовать код с sqlc (часто сочетая его с инструментом миграций, таким как Flyway или Golang-Migrate для управления версиями схемы, поскольку sqlc сам по себе не управляет схемой). Существует официальный Slack/Discord для sqlc, где можно задавать вопросы, и вопросы на GitHub обычно получают внимание. Многие из распространенных паттернов (например, как обрабатывать нулевые значения или JSON-поля) документированы в документации sqlc или есть сообщества блоги. Стоит отметить: sqlc не специфичен для Go — он может генерировать код на других языках (например, TypeScript, Python). Это расширяет его сообщество за пределы Go, но в Go в частности он широко уважаем. Если вы выберете sqlc, вы в хорошей компании: его используют в производстве многие стартапы и крупные компании (как следует из демонстраций сообщества и спонсоров, перечисленных в репозитории). Ключевой момент в экосистеме заключается в том, что поскольку sqlc не предоставляет функций времени выполнения, как ORM, возможно, вам потребуется подключить другие библиотеки для таких вещей, как транзакции (хотя вы можете легко использовать sql.Tx с sqlc) или, возможно, легковесную обертку DAL. На практике большинство используют sqlc вместе со стандартной библиотекой (для транзакций, отмены контекста и т.д. вы используете идиомы database/sql непосредственно в вашем коде). Это означает меньшую привязку к поставщику — переход от sqlc просто означает написание собственного слоя данных, что так же сложно, как написание SQL (которое вы уже сделали). В целом, сообщество и поддержка sqlc крепкие, многие рекомендуют его как “обязательный к использованию” для проектов Go, взаимодействующих с SQL-базами данных, благодаря своей простоте и надежности.

Набор функций и расширяемость

Каждый из этих инструментов предлагает разный набор функций. Здесь мы сравниваем их возможности и то, насколько они расширяемы для сложных сценариев использования:

  • Функции GORM: GORM стремится быть полнофункциональным ORM. Его список функций обширен:
  • Поддержка нескольких баз данных: Первоклассная поддержка PostgreSQL, MySQL, SQLite, SQL Server и других. Переключение между базами данных обычно так же просто, как изменение драйвера подключения.
  • Миграции: Вы можете автоматически мигрировать схему из своих моделей (db.AutoMigrate(&User{}) создаст или изменит таблицу users). Хотя это удобно, будьте осторожны с автоматической миграцией в продакшене — многие используют её для разработки и имеют более контролируемые миграции для продакшена.
  • Отношения: Тэги структур GORM (gorm:"foreignKey:...,references:...") позволяют определять отношения один-ко-многим, многие-ко-многим и т.д. Он может обрабатывать промежуточные таблицы для отношений многие-ко-многим и имеет Preload для предварительной загрузки отношений. GORM по умолчанию использует ленивую загрузку (т.е. отдельные запросы) при доступе к связанным полям, но вы можете использовать Preload или Joins для настройки этого. Новые версии также имеют API на основе обобщений для упрощения запросов ассоциаций.
  • Транзакции: GORM имеет удобный обёртку для транзакций (db.Transaction(func(tx *gorm.DB) error { ... })) и даже поддержку вложенных транзакций (точек сохранения).
  • Хуки и колбэки: Вы можете определять методы, такие как BeforeCreate, AfterUpdate в своих структурах моделей, или регистрировать глобальные колбэки, которые GORM будет вызывать на определённых этапах жизненного цикла. Это полезно для автоматического установки временных меток или поведения мягкого удаления.
  • Расширяемость: Система плагинов GORM (интерфейс gorm.Plugin) позволяет расширять его функциональность. Примеры: пакет gorm-gen генерирует безопасные для типов методы запросов (если вы предпочитаете проверки запросов на этапе компиляции), или сообщественные плагины для аудита, многократного использования и т.д. Вы также можете использовать сырой SQL в любое время через db.Raw("SELECT ...", params).Scan(&result).
  • Другие удобства: GORM поддерживает составные первичные ключи, встраивание моделей, полиморфные ассоциации и даже разрешитель схем для использования нескольких баз данных (для реплик чтения, шардинга и т.д.).

В целом, GORM высоко расширяем и функционально богат. Практически любая функция ORM, которую вы можете пожелать, имеет либо встроенный механизм, либо документированный паттерн в GORM. Цена этой широты — сложность и некоторая жёсткость (часто вам нужно соответствовать способу работы GORM). Но если вам нужна универсальная решение, GORM предлагает его.

  • Функции Ent: Философия Ent основана на схеме как коде. Ключевые функции включают:
  • Определение схемы: Вы можете определять поля с ограничениями (уникальность, значения по умолчанию, перечисления и т.д.), и связи (отношения) с кардинальностью (один-ко-многим и т.д.). Ent использует это для генерации кода и также может генерировать SQL-миграции для вас (есть компонент ent/migrate, который может производить разностные SQL между текущей схемой и желаемой схемой).
  • Безопасность типов и валидация: Поскольку поля строго типизированы, вы не можете, например, случайно установить строковое значение в целочисленное поле. Ent также позволяет использовать пользовательские типы полей (например, вы можете интегрировать sql.Scanner/driver.Valuer для полей JSONB или других сложных типов).
  • Построитель запросов: Сгенерированный API запросов Ent охватывает большинство SQL-конструкций: вы можете выполнять выборки, фильтрации, упорядочивание, ограничения, агрегации, соединения через связи и даже подзапросы. Он выразителен — например, вы можете написать client.User.Query().WithOrders(func(q *ent.OrderQuery) { q.Limit(5) }).Where(user.StatusEQ(user.StatusActive)).All(ctx), чтобы получить активных пользователей с их первыми 5 заказами, за один раз.
  • Транзакции: Клиент Ent поддерживает транзакции, предоставляя транзакционный вариант клиента (через tx, err := client.Tx(ctx), который даёт ent.Tx, который вы можете использовать для выполнения нескольких операций, а затем подтверждения или отката).
  • Хуки и промежуточное ПО: Ent позволяет регистрировать хуки на операциях создания/обновления/удаления — это похоже на перехватчики, где вы можете, например, автоматически заполнить поле или обеспечить соблюдение пользовательских правил. Также есть промежуточное ПО для клиента, чтобы оборачивать операции (полезно для логирования, инструментарии).
  • Расширяемость: Хотя Ent не имеет “плагинов” как таковых, его генерация кода шаблонизирована, и вы можете писать пользовательские шаблоны, если вам нужно расширять сгенерированный код. Многие продвинутые функции были реализованы таким образом: например, интеграция с OpenTelemetry для трассировки вызовов БД или генерация GraphQL-резолверов из схемы Ent и т.д. Ent также позволяет смешивать сырой SQL при необходимости через пакет entsql или получение нижнего драйвера. Возможность генерировать дополнительный код означает, что команды могут использовать Ent как основу и накладывать свои собственные паттерны сверху, если это необходимо.
  • Интеграция с GraphQL/REST: Большим преимуществом подхода Ent на основе графа является то, что он хорошо сочетается с GraphQL — ваша схема Ent может быть почти напрямую отображена на схему GraphQL. Существуют инструменты для автоматизации этого. Это может быть выигрышем в производительности, если вы создаёте сервер API.
  • Настройки производительности: Ent, например, может загружать связанные связи пакетами, чтобы избежать запросов N+1 (у него есть API предварительной загрузки .With<EdgeName>()). Он будет использовать JOIN или дополнительные запросы под капотом оптимизированным образом. Также можно включить кэширование результатов запросов (в памяти), чтобы избежать обращения к БД для идентичных запросов в короткий промежуток времени.

В итоге, набор функций Ent ориентирован на крупномасштабные, поддерживаемые проекты. Он может не иметь некоторых готовых возможностей GORM (например, автоматическая миграция схемы GORM против сгенерированных скриптов миграций Ent — Ent выбирает разделение этой задачи), но компенсирует это мощными инструментами разработки и безопасностью типов. Расширяемость в Ent заключается в генерации того, что вам нужно — она очень гибкая, если вы готовы разобраться, как работает entc (генерация кода).

  • Функции Bun: Bun сосредоточен на том, чтобы быть мощным инструментом для тех, кто знает SQL. Некоторые функции и точки расширяемости:
  • Построитель запросов: В основе Bun лежит удобный построитель запросов, который поддерживает большинство SQL-конструкций (SELECT с соединениями, CTE, подзапросы, а также INSERT, UPDATE с поддержкой массовых операций). Вы можете начать запрос и глубоко его настроить, даже вставляя сырой SQL (.Where("some_condition (?)", value)).
  • Специфичные для PostgreSQL функции: Bun имеет встроенную поддержку типов массивов Postgres, JSON/JSONB (отображение на []<type> или map[string]interface{} или пользовательские типы), и поддерживает сканирование в составные типы. Например, если у вас есть столбец JSONB, вы можете отобразить его на структуру Go с тегами json:, и Bun будет обрабатывать сериализацию для вас. Это преимущество перед некоторыми ORM, которые рассматривают JSONB как непрозрачный.
  • Отношения/Предварительная загрузка: Как показано ранее, Bun позволяет определять теги структур для отношений, а затем вы можете использовать .Relation("FieldName") в запросах для соединения и загрузки связанных сущностей. По умолчанию .Relation использует LEFT JOIN (вы можете симулировать внутренние соединения или другие типы, добавляя условия). Это даёт вам точный контроль над тем, как связанные данные извлекаются (в одном запросе или нескольких). Если вы предпочитаете ручные запросы, вы всегда можете просто написать соединение в SQL-построителе Bun напрямую. В отличие от GORM, Bun никогда не будет автоматически загружать отношения без вашего запроса, что избегает нежелательных проблем N+1.
  • Миграции: Bun предоставляет отдельный набор инструментов для миграций (в github.com/uptrace/bun/migrate). Миграции определяются на Go (или строках SQL) и версионируются. Это не так автомагически, как автоматическая миграция GORM, но это надёжно и хорошо интегрируется с библиотекой. Это отлично для явного контроля над изменениями схемы.
  • Расширяемость: Bun построен на интерфейсах, похожих на database/sql — он даже оборачивает *sql.DB. Вы можете использовать любые инструменты более низкого уровня с ним (например, вы могли бы выполнить запрос *pgx.Conn, если это необходимо, и всё равно сканировать в модели Bun). Bun позволяет пользовательским сканерам значений, поэтому если у вас есть сложный тип, который вы хотите хранить, вы реализуете sql.Scanner/driver.Valuer, и Bun будет использовать его. Для расширения функциональности Bun, поскольку он относительно прост, многие люди просто пишут дополнительные вспомогательные функции поверх API Bun в своих проектах, а не отдельные плагины. Библиотека сама по себе развивается, поэтому новые функции (например, дополнительные вспомогательные методы запросов или интеграции) добавляются поддерживающими.
  • Другие функции: Bun поддерживает отмену контекста (все запросы принимают ctx), у него есть пул соединений через database/sql под капотом (настраиваемый там), и поддерживает кэширование подготовленных выражений (через драйвер, если используется pgx). Есть также удобная функция, где Bun может сканировать в указатели структур или примитивные срезы легко (например, выбор одного столбца в []string напрямую). Это небольшие удобства, которые делают Bun приятным для тех, кто не любит повторяющийся код сканирования.

В итоге, набор функций Bun покрывает 90% потребностей ORM, но с акцентом на прозрачность и производительность. Он может не иметь всех колокольчиков и свистков (например, он не делает автомагические обновления схемы или не имеет активного паттерна записи), но он предоставляет строительные блоки для реализации всего, что вам нужно сверху. Поскольку он молодой, ожидайте, что его набор функций будет продолжать расширяться, руководствуясь реальными сценариями использования (поддерживающие часто добавляют функции по запросам пользователей).

  • Функции sqlc: функции sqlc существенно отличаются, так как это генератор:
  • Полная поддержка SQL: главная функция заключается в том, что вы используете функции PostgreSQL напрямую. Если PostgreSQL поддерживает какую-то функцию, вы можете использовать её в sqlc. Это включает CTE, оконные функции, операторы JSON, пространственные запросы (PostGIS) и т.д. Нет необходимости в том, чтобы библиотека сама реализовывала что-то особенное для использования этих функций.
  • Сопоставление типов: sqlc умно сопоставляет типы SQL с типами Go. Он обрабатывает стандартные типы и позволяет вам настраивать или расширять сопоставления для пользовательских типов или перечислений. Например, тип Postgres UUID может быть сопоставлен с типом github.com/google/uuid, если вы хотите, или доменный тип может быть сопоставлен с подлежащим типом Go.
  • Обработка NULL: он может генерировать sql.NullString или указатели для nullable-колонок, в зависимости от ваших предпочтений, поэтому вам не нужно бороться со сканированием NULL.
  • Пакетные операции: хотя sqlc сам по себе не предоставляет высокоуровневый API для пакетной обработки, вы можете написать SQL для массового вставки и сгенерировать код для него. Или вызвать хранимую процедуру — это ещё одна функция: поскольку это SQL, вы можете использовать хранимые процедуры или функции БД, и sqlc сгенерирует обёртку на Go.
  • Многооперационные запросы: вы можете поместить несколько SQL-операций в один именованный запрос, и sqlc выполнит их в транзакции (если драйвер поддерживает это), возвращая результаты последнего запроса или то, что вы укажете. Это способ, например, сделать что-то вроде “создать и затем выбрать” в одном вызове.
  • Расширяемость: будучи компилятором, расширяемость выражается в виде плагинов для новых языков или сообщества, поддерживающего новые SQL-конструкции. Например, если выйдет новый тип данных PostgreSQL, sqlc может быть обновлён для поддержки его сопоставления. В вашем приложении вы можете расширять sqlc, написав обёрточные функции. Поскольку сгенерированный код находится под вашим контролем, вы могли бы его изменить — хотя обычно вы этого не делаете, а корректируете SQL и перегенерируете. Если нужно, вы всегда можете смешивать сырые вызовы database/sql с sqlc в вашем коде (нет конфликта, так как sqlc просто выводит некоторый Go-код).
  • Минимальные зависимости во время выполнения: код, который генерирует sqlc, обычно зависит только от стандартного database/sql (и конкретного драйвера, такого как pgx). Нет тяжёлой библиотеки времени выполнения; это просто некоторые вспомогательные типы. Это означает нулевые накладные расходы в продакшене со стороны библиотеки — вся работа выполняется на этапе компиляции. Это также означает, что вы не получите функции, такие как объектный кеш или карта идентификаторов (как у некоторых ORM) — если вам нужно кэширование, вы реализуете его в своём сервисном слое или используете отдельную библиотеку.

В сущности, “функция” sqlc заключается в том, что он сужает разрыв между вашей SQL-базой данных и вашим кодом на Go без добавления чего-то лишнего между ними. Это специализированный инструмент — он не делает миграции, не отслеживает состояние объектов и т.д. по дизайну. Эти задачи оставлены другим инструментам или разработчику. Это привлекательно, если вы хотите лёгкий слой данных, но если вы ищете решение “всё в одном”, которое обрабатывает всё от схемы до запросов до отношений в коде, то sqlc в одиночку не подходит — вы будете использовать его в сочетании с другими паттернами или инструментами.

Подведём итог этого раздела: GORM — это мощный набор функций и очевидный выбор, если вам нужен зрелый, расширяемый ORM, который можно адаптировать через плагины. Ent предлагает современный, типизированный подход к функциям, делая акцент на корректности и поддержке (с такими вещами, как генерация кода, хуки и интеграция в API-слои). Bun предоставляет основы и делает ставку на знание SQL, что облегчает его расширение путём написания большего количества SQL или небольших обёрток. sqlc сводит функции к базовому уровню — он по сути столь же функционален, как и SQL сам по себе, но всё, что выходит за его рамки (кэширование и т.д.), вам нужно добавлять самостоятельно.

Пример кода: Операции CRUD в каждом ORM

Ничто лучше не иллюстрирует различия, чем сравнение того, как каждый инструмент обрабатывает базовые операции CRUD для простой модели. Предположим, у нас есть модель User с полями ID, Name и Email. Ниже приведены кодовые фрагменты для создания, чтения, обновления и удаления пользователя в каждой библиотеке, используя PostgreSQL в качестве базы данных.

Примечание: В всех случаях предполагается, что у нас уже установлено соединение с базой данных (например, db или client), и обработка ошибок опущена для краткости. Цель — сравнить API и словоохотливость каждого подхода.

GORM (стиль Active Record)

    // Определение модели
    type User struct {
        ID    uint   `gorm:"primaryKey"`
        Name  string
        Email string
    }

    // Создание нового пользователя
    user := User{Name: "Alice", Email: "alice@example.com"}
    db.Create(&user)                      // INSERT INTO users (name,email) VALUES ('Alice','alice@example.com')

    // Чтение (поиск по первичному ключу)
    var u User
    db.First(&u, user.ID)                // SELECT * FROM users WHERE id = X LIMIT 1

    // Обновление (один столбец)
    db.Model(&u).Update("Email", "alice_new@example.com")
    // (генерирует: UPDATE users SET email='alice_new@example.com' WHERE id = X)

    // Удаление
    db.Delete(&User{}, u.ID)             // DELETE FROM users WHERE id = X

Ent (Codegen, fluent API)

    // (Схема Ent определена в другом месте и сгенерирован код. Предположим, что client — это ent.Client)

    // Создание нового пользователя
    u, err := client.User.
        Create().
        SetName("Alice").
        SetEmail("alice@example.com").
        Save(ctx)
    // SQL: INSERT INTO users (name, email) VALUES ('Alice','alice@example.com') RETURNING id, name, email

    // Чтение (по ID)
    u2, err := client.User.Get(ctx, u.ID)
    // SQL: SELECT * FROM users WHERE id = X

    // Обновление (одно поле)
    u3, err := client.User.
        UpdateOneID(u.ID).
        SetEmail("alice_new@example.com").
        Save(ctx)
    // SQL: UPDATE users SET email='alice_new@example.com' WHERE id = X RETURNING id, name, email

    // Удаление
    err = client.User.DeleteOneID(u.ID).Exec(ctx)
    // SQL: DELETE FROM users WHERE id = X

Bun

    // Определение модели
    type User struct {
        bun.BaseModel `bun:"table:users"`
        ID   int64    `bun:",pk,autoincrement"`
        Name string   `bun:",notnull"`
        Email string  `bun:",unique,notnull"`
    }

    // Создание нового пользователя
    user := &User{Name: "Alice", Email: "alice@example.com"}
    _, err := db.NewInsert().Model(user).Exec(ctx)
    // INSERT INTO users (name, email) VALUES ('Alice','alice@example.com')

    // Чтение (поиск по ID)
    var u User
    err = db.NewSelect().Model(&u).
          Where("id = ?", user.ID).
          Scan(ctx)
    // SELECT * FROM users WHERE id = X

    // Обновление (один столбец)
    _, err = db.NewUpdate().Model(&u).
          Set("email = ?", "alice_new@example.com").
          Where("id = ?", u.ID).
          Exec(ctx)
    // UPDATE users SET email='alice_new@example.com' WHERE id = X

    // Удаление
    _, err = db.NewDelete().Model((*User)(nil)).
          Where("id = ?", u.ID).
          Exec(ctx)
    // DELETE FROM users WHERE id = X

sqlc


    // Предположим, что мы написали SQL в файлах, и sqlc сгенерировал структуру Queries с методами.
    queries := New(db)  // New принимает *sql.DB (или pgx.Conn) и возвращает сгенерированные Queries

    // Создание нового пользователя (сгенерированный метод выполняет INSERT и сканирует результат)
    newUser, err := queries.CreateUser(ctx, "Alice", "alice@example.com")
    // SQL в queries.sql -> INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email

    // Чтение (по ID)
    user, err := queries.GetUser(ctx, newUser.ID)
    // SQL -> SELECT id, name, email FROM users WHERE id = $1

    // Обновление (email по ID)
    updatedUser, err := queries.UpdateUserEmail(ctx, newUser.ID, "alice_new@example.com")
    // SQL -> UPDATE users SET email=$2 WHERE id = $1 RETURNING id, name, email

    // Удаление
    err = queries.DeleteUser(ctx, newUser.ID)
    // SQL -> DELETE FROM users WHERE id = $1

Как показывает приведенный выше код, у каждого подхода свой стиль:

  • GORM использует методы структур и цепочку вызовов на db.Model(&obj) или непосредственно на объекте db. Он заполняет структуру возвращаемыми значениями (например, после Create, user.ID устанавливается). Также он скрывает детали SQL по умолчанию — вы обычно не видите запрос, если не отлаживаете.

  • Ent использует сгенерированный API с цепочкой вызовов. Обратите внимание, как методы вроде Create().SetX().Save(ctx) или UpdateOneID(id).SetX().Save(ctx) четко разделяют фазы построения и выполнения. Ent возвращает объекты ent.Type (которые соответствуют строкам) и ошибки, аналогично тому, как SQL-запрос либо возвращает результаты, либо ошибку.

  • Bun требует более явного указания (например, использования Set("email = ?", ...) для обновлений), что очень похоже на написание SQL, но с синтаксисом Go. После вставки структура user не заполняется автоматически новым ID, если не добавить предложение RETURNING (Bun поддерживает .Returning() при необходимости). Пример выше упрощает вещи.

  • sqlc выглядит как вызов любых функций Go. Мы вызываем queries.CreateUser и т.д., а под капотом эти методы выполняют подготовленные инструкции. SQL написано во внешних файлах, поэтому, хотя вы не видите его в коде Go, у вас есть полный контроль над ним. Возвращаемые объекты (например, newUser) — это обычные структуры Go, сгенерированные sqlc для моделирования данных.

Можно наблюдать различия в словоохотливости (GORM очень лаконичен; Bun и sqlc требуют немного больше набора текста в коде или SQL) и различия в стиле (Ent и GORM предлагают более высокоуровневые абстракции, тогда как Bun и sqlc ближе к сырым запросам). В зависимости от ваших предпочтений, вы можете отдавать предпочтение явности перед лаконичностью или наоборот.


Кратко

Выбор “правильного” ORM или библиотеки базы данных в Go сводится к потребностям вашего приложения и предпочтениям вашей команды:

  • GORM — отличный выбор, если вы хотите проверенный ORM, который делает многое за вас. Он особенно хорош для быстрого разработки CRUD-приложений, где удобство и богатый функционал важнее, чем максимальная производительность. Поддержка сообщества и документация отличные, что помогает сгладить острые углы его кривой обучения. Будьте внимательны при использовании его функций (например, используйте Preload или Joins, чтобы избежать ленивой загрузки) и ожидайте некоторую нагрузку. Взамен вы получаете продуктивность и решение для большинства проблем. Если вам нужны такие вещи, как поддержка нескольких баз данных или обширная экосистема плагинов, GORM вас покроет.

  • Ent привлекает тех, кто ценит типобезопасность, ясность и поддерживаемость. Он хорошо подходит для крупных кодовых баз, где изменения схемы происходят часто, и вы хотите, чтобы компилятор помогал вам находить ошибки. Ent может потребовать больше предварительной работы (определение схем, запуск генерации), но это окупается по мере роста проекта — ваш код остается надежным и удобным для рефакторинга. С точки зрения производительности, он может эффективно обрабатывать тяжелые нагрузки и сложные запросы, часто лучше, чем активные ORM, благодаря оптимизированной генерации SQL и кэшированию. Ent немного менее “готовый к использованию” для быстрых скриптов, но для долгосрочных сервисов он предоставляет надежный, масштабируемый фундамент. Его современный подход (и активная разработка) делают его перспективным выбором.

  • Bun идеален для разработчиков, которые говорят: “Я знаю SQL и просто хочу легковесную обертку вокруг него”. Он отказывается от некоторых “магических” возможностей, чтобы дать вам контроль и скорость. Если вы создаете сервис, чувствительный к производительности, и не боитесь быть явными в коде доступа к данным, Bun — привлекательный вариант. Это также хороший компромисс, если вы мигрируете с сырых database/sql+sqlx и хотите добавить структуру без значительных потерь в эффективности. Цена — меньшее сообщество и меньше высокоуровневых абстракций — вам придется писать немного больше кода вручную. Но как говорят в документации Bun, он не мешает вам. Используйте Bun, когда вы хотите ORM, который чувствуется как использование SQL напрямую, особенно для проектов, ориентированных на PostgreSQL, где вы можете использовать специфичные для базы данных функции.

  • sqlc находится в своей собственной категории. Это идеально для команды Go, которая говорит: “Мы не хотим ORM, мы хотим проверку на этапе компиляции для нашего SQL”. Если у вас сильные навыки SQL и вы предпочитаете управлять запросами и схемой на SQL (возможно, у вас есть DBAs или вы просто любите создавать эффективный SQL), sqlc, вероятно, повысит вашу продуктивность и уверенность. Производительность практически оптимальна, так как ничего не стоит между вашим запросом и базой данных. Единственные причины не использовать sqlc — это если вы действительно не любите писать SQL или если ваши запросы настолько динамичны, что написание многих вариантов становится обременительным. Даже в этом случае вы можете использовать sqlc для большинства статических запросов и обрабатывать несколько динамичных случаев с другим подходом. sqlc также хорошо сочетается с другими — он не исключает использование ORM в частях вашего проекта (некоторые проекты используют GORM для простых вещей и sqlc для критических путей, например). Короче говоря, выбирайте sqlc, если вы цените явность, нулевую нагрузку и типобезопасный SQL — это мощный инструмент в вашем арсенале.

Наконец, стоит отметить, что эти инструменты не взаимно исключают друг друга в экосистеме. У каждого есть свои плюсы и минусы, и в духе прагматизма Go многие команды оценивают компромиссы в каждом конкретном случае. Не редкость начинать с ORM вроде GORM или Ent для скорости разработки, а затем использовать sqlc или Bun для конкретных горячих путей, которые требуют максимальной производительности. Все четыре решения активно поддерживаются и широко используются, поэтому в целом нет “неправильного” выбора — это вопрос правильного выбора для вашего контекста. Надеюсь, это сравнение дало вам более четкое представление о том, как GORM, Ent, Bun и sqlc соотносятся друг с другом, и помогло вам принять обоснованное решение для вашего следующего проекта на Go.

Полезные ссылки