Вам, наверное, знаком подход радикальной простоты, который заключается в том, чтобы иметь как можно меньше систем и наименьшее количество строк кода и конфигурации. Это снижает затраты на техническое обслуживание и делает изменения дешёвыми и лёгкими. Но радикальная простота не означает использование ассемблерного кода или C.
Так подходит ли SQL для этой задачи или лучше использовать что-то другое?
При написании серверного кода для чтения из базы данных разработчики обычно используют прямой SQL либо ORM. ORM экономит время на написании SQL-кода, но снижает производительность и увеличивает потребность в большем количестве классов. Прямой SQL быстрее и содержит меньше типовых строк кода, но его сложнее изменить.
В то же время многие разработчики предпочитают перейти на React и SPA с GraphQL, что позволяет использовать гораздо меньше кода для написания серверного приложения, продуктом которого является HTML. Но GraphQL кажется концепцией, которая упрощает чтение данных из одного или нескольких источников данных. Поэтому с появлением GraphQL я задался вопросом, будет ли GraphQL лучше ORM для записи представлений данных в приложениях. В лучшем случае вы пишете HTML-шаблон и запрос GraphQL без какого-либо другого кода.
Мы сфокусируемся на той части кода, которая производит чтение данных, а не запись. И я считаю, что они могут отличатся. Зачем использовать разный код для чтения и записи в базу данных? По моему опыту, чтение и запись масштабируются по-разному, и, хотя использование одного и того же кода для чтения и записи может показаться преимуществом, это часто усложняет изменение кода. Мы хотим видеть, что та часть веб-приложения, которая выполняет операции чтения, может извлечь выгоду из GraphQL.
В качестве примера используем простое приложение для задач «Task app», с пользователями и прикреплёнными к ним задачами, у которых есть статус. Мы реализуем страницу, на которой отображаются все задачи для одного пользователя с указанием пользователя и статуса.
Давайте посмотрим, как использовать GraphQL для рендеринга HTML-шаблонов на стороне сервера. Мы сравниваем решение GraphQL с базовым SQL и с ORM. Я использую Golang с Graphjin для GraphQL, GORM для ORM и PGX для простого SQL-решения.
SQL
Создаём БД SQL:
CREATE TABLE users( id BIGINT unique PRIMARY KEY, name TEXT ); CREATE TABLE status ( id INT unique, status TEXT ); CREATE TABLE tasks ( id BIGINT PRIMARY KEY, title TEXT, user_id bigint REFERENCES users(id), status_id INT REFERENCES status(id) );
SQL с PGX
Начнём с написания простого SQL-решения. Для этого используем PGX в качестве библиотеки Golang. Нам нужна модель для размещения данных, в которую мы передаём HTML-шаблон. Модель — это просто данные из задач:
type PgxTaskList struct { Tasks []PgxTask } type PgxTask struct { Id int64 Title string Status string User string }
Затем нам нужен некоторый SQL-код для чтения из базы данных. Код может быть обобщен и существуют библиотеки, которые устраняют шаблонность, например, с помощью функции, которая сопоставляет результирующие строки базы данных со структурой.
Но в этом примере мы пишем низкоуровневый SQL-код в качестве базового уровня для сравнения.
rows, err := pool.Query(context.Background(), "select t.id,t.title,u.name,s.status from tasks t, users u, status s where t.user_id=$1 and t.user_id=u.id and t.status_id=s.id" , user) if err != nil { panic(err) } p := pgxbench.PgxTaskList{} tasks := make([]pgxbench.PgxTask, 0) for rows.Next() { var id int64 var title string var status string var user string err := rows.Scan(&id, &title, &user, &status) if err != nil { panic(err) } t := pgxbench.PgxTask{ Id: id, Title: title, User: user, Status: status, } tasks = append(tasks, t) } p.Tasks = tasks
Сопоставление строк SQLX
На один уровень выше обычного SQL используется библиотека rowmapper, подобная sqlx в Golang. Структуры будут такими же, как и в SQL, но шаблонный код будет сокращён до:
err := db.Select(&tasks, "select t.id Id,t.title Title,u.name Name,s.status Status from tasks t, users u, status s where t.user_id=u.id and t.status_id=s.id and t.user_id=$1", user)
Это стандартная практика сокращения кода. SQLX использует отражение для сопоставления структур, что, как я предполагаю, будет стоить некоторой производительности. Существуют и другие библиотеки с функцией rowmapper, которые имеют больше кода, но лучшую производительность. Ох уж эти компромиссы!
Gorm
GORM — это объектно-реляционный mapper (ORM) для Golang. ORM преобразует вызовы методов в SQL и помещает данные в модель. В случае использования GORM нам нужна модель:
type GormTaskList struct { Tasks []Task } type User struct { ID uint Name string } type Status struct { ID uint Status string } type Task struct { ID uint Title string UserID uint User User StatusID uint Status Status }
Затем мы можем запросить базу данных с помощью одной строки:
db.Where("user_id = ?", userId).Preload("User").Preload("Status").Find(&tasks)
и передать данные в шаблон. Мы используем preload для зависимых объектов, в противном случае нам пришлось бы загружать их позже или при обращении к ним. Я предполагаю, что таким образом мы получаем наилучшую производительность.
Генерация JSON в Postgres
Вы можете напрямую сгенерировать JSON в базе данных, а затем отобразить его в шаблоне. С помощью небольшого трансформатора, который преобразует $ и $$ в row_to_json, json_agg и json_build_object:
WITH tasks AS $( SELECT t.id id, t.title title, u.name name, s.status status FROM tasks t, users u, status s WHERE t.user_id=u.id AND t.status_id=s.id AND t.user_id=$1 ) $$( 'tasks', (SELECT * from tasks) )
код Go — это просто:
row, err := pool.Query( context.Background(), query, args...) if err != nil { panic(err) } defer row.Close() row.Next() var json map[string]interface{} if err := row.Scan(&json); err != nil { panic(err) } return c.Render(http.StatusOK, template, json)
Этот код одинаков для всех обработчиков и может быть использован повторно. Изменяется только запрос и шаблон.
Чтение задач с помощью GraphQL
Мы используем Graphing в качестве серверной библиотеки Golang для выполнения GraphQL в базе данных Postgresql. С GraphQL нам нужен только один запрос:
query GetTasks { tasks(where: { user_id: $userId } ){ id title user { id name } status { id status } } }
Запрос возвращает JSON, который мы можем напрямую ввести в шаблон.
Производительность
Предостережение: я не специалист по бенчмаркингу Go. Я также не специалист по pgx, sqlx, GORM или Graphjin. Я заинтересован не в том, чтобы подделать бенчмарк, а в том, чтобы найти способ ускорить выполнение кода или оптимизировать конфигурации.
Тесты проводились на WSL/Windows 11, Postgres 15, Go 1.19.3, Ryzen 3900x/12c, 32gb/3600, твердотельном накопителе WD SN850.
Анализ производительности каждого из решений приводит к некоторым сюрпризам. Общая картина не вызывает удивления, обычный SQL самый быстрый, GraphQL самый медленный, а SQLX mapper и ORM находятся между ними. Однако удивительно то, насколько хуже работает mapper по сравнению с рукописным SQL. Второй сюрприз заключается в том, насколько близки ORM и GraphQL. Я бы подумал, что GraphQL намного хуже по производительности по сравнению с ORM.
Основными драйверами может быть то, что GraphQL и ORM создают больше объектов и запускается GC. Кроме того, ORM и GraphQL создают все более сложные запросы. Их можно было бы изучить в будущем.
Используем k6 для нагрузочного тестирования приложения. Все страницы делают то же самое, загружают задачи для одного случайного пользователя в память и отображают их в HTML — в обычном SQL, с помощью SQLX, с помощью GORM и с помощью Graphjin. БД небольшая, но реалистичная для небольшого стартапа, 10000 задач в БД, 100 пользователей, 100 задач/пользователь, 5 статусов. При использовании индексов я не думаю, что размер таблицы задач оказал бы большое влияние.
(“concurrent users” == vu в k6)

И P90 мс, которые требуются для одного запроса:

Сравнение
Строки кода для решения GraphQL самые маленькие, вам нужен только запрос и никакого кода Go. Это делает добавление атрибута в представление очень простым: добавьте в запрос GraphQL и в шаблон. Далее следует решение ORM с изменениями в модели и HTML-шаблоне. ORM нужны изменения в структуре и SQL, SQL-решению нужны изменения в запросе, сопоставлении и структуре. И все они нуждаются в изменениях в HTML.
Решение GraphQL выглядит наиболее чистым с точки зрения количества строк кода и изменений, необходимых для добавления одного нового атрибута.
У GraphQL есть проблемы с производительностю, но это не так плохо, как я предполагал. Если вы уже используете ORM, игнорируя разделение чтения/записи, ORM может быть лучшим решением, но оказывает большее влияние на производительность, чем думают многие люди. Обычный SQL является самым быстрым, но требует наибольшего количества изменений, и его труднее читать и понимать. Производительность GraphQL и ORM непрозрачна, сложнее для понимания и оптимизации.
Самым большим недостатком GraphQL, по-видимому, является добавление ещё одной зависимости. Graphjin — это немаленький пакет, который сам по себе имеет множество сторонних зависимостей. Другая зависимость противоречит радикальной простоте и поэтому является компромиссом.
Но из-за скорости разработки и простоты внесения изменений, при сохранении достаточной производительности, я рассмотрю GraphQL для своих будущих проектов разработки на стороне сервера.
Спасибо за внимание!
