Когда речь заходит об использовании SQL, экосистеме Rust есть что нам предложить: к счастью, нам доступны несколько отличных готовых к использованию решений. SQLx — это чисто асинхронный, независящий от рантайма SQL-крейт, позволяющий использовать проверяемые во время компиляции запросы без какого-либо DSL. Являясь одним из наиболее популярных способов использования SQL в Rust, он обладает следующими преимуществами:
-
Он совместим со всеми вашими любимыми разновидностями SQL (MySQL, SQLite, Postgres).
-
Проверка запросов во время компиляции обеспечивает корректность этих самых запросов и типов, которые в них задействованы.
-
Поддержка дополнительных функций, таких как listen/notify из Postgres.
-
Множество различных способов построения и использования запросов.
-
С помощью SQLx вы также можете создать свой собственный конструктор запросов!
Давайте же посмотрим на SQLx в действии!
Начало работы
Для начала вам необходимо добавить sqlx в вашу Rust-программу:
cargo add sqlx
Также необходимо установить sqlx-cli, официальный CLI SQLx, который, помимо всего прочего, облегчает управление миграциями. Установить его можно, выполнив приведенную ниже команду:
cargo install sqlx-cli
Миграции
Первый шаг — миграция. При желании можно просто создать таблицы вручную — но это потребует много времени и усилий… и нужно будет постоянно помнить, что конкретно мы сделали на этом этапе! К счастью, мы можем написать .sql-файлы, представляющие наши миграции, и затем переносить их в любую используемую нами базу данных либо с помощью sqlx-cli, либо с помощью команды sqlx::execute. Простая SQL-схема может выглядеть следующим образом:
-- таблица здесь будет создана только в том случае, если она еще -- не существует, что позволяет избежать проблемы с перезатиранием таблиц CREATE TABLE IF NOT EXISTS foo ( id SERIAL PRIMARY KEY, message TEXT );
Если это корректный SQL, то какой бы метод вы ни использовали, он успешно выполнится, создав в вашей базе данных таблицу _sqlx_migrations с примененным списком миграций.
Ваша встроенная команда миграции может выглядеть следующим образом:
В качестве рекомендации скажу вам, что сам я использую sqlx-cli, где выполняю команду sqlx migrate -r add <filename>. Эта команда, по сути, добавляет новую миграцию, но флаг -r позволяет в любой момент отменить при возникновении каких-либо ошибок. Это удобный способ, позволяющий откатить все назад, если что-то пойдет не так после развертывания новой миграции в продакшене.
Запросы
Мы по умолчанию можем использовать чистые SQL-запросы, просто выполняя запрос на нашем пуле соединений:
let query = sqlx::query("SELECT * FROM TABLE") .execute(&pool) .await .unwrap();
По умолчанию SQLx поощряет использование связанных параметров, которые очень важны для предотвращения SQL-инъекций — достаточно просто добавить их в запрос (подробнее об этом можно прочитать здесь):
sqlx::query("INSERT INTO TABLE (foo) VALUES ($1)") .bind("bar".to_string()) .execute(&pool) .await .unwrap();
Теперь предположим, что вы пишете запрос, который возвращает что-то. Когда вы получаете строки из этого запроса, вам, скорее всего, придется брать каждое значение по отдельности — в небольших масштабах это еще куда ни шло, но когда вы используете fetch_all, вам придется задействовать итератор, чтобы получить то, что вам нужно из каждой строки. К счастью, создатели SQLx понимали это и предусмотрели макрос для извлечения вектора со структурами из вектора строк SQL — вы можете использовать query_as для привязки возвращаемых результатов к структуре, которая использует #[derive(Sqlx::FromRow)].
Выглядеть это будет следующим образом:
#[derive(sqlx::FromRow)] struct Foo { id: i32, message: String } async fn foo(pool: PgPool) -> Vec<Foo> { let res = sqlx::query_as::<_, Foo>("SELECT * FROM FOO") .fetch_all(&pool).await.unwrap(); Ok(res) }
Вам нужно сделать что-то более сложное? Для построения запросов можно использовать тип QueryBuilder. Хоть он отлично подходит для программного добавления динамических фрагментов в запросы, следует быть осторожным при его использовании, поскольку в нем есть методы для добавления значений, не являющихся связанными параметрами, — в идеале, если вы не уверены в безопасности используемого метода, лучше использовать push_bind.
Пример использования:
const BIND_LIMIT: usize = 65535; // Значения здесь могут выдаваться бесконечно! let records = (0..).map(|i| Foo { id: i, message: format!("This is note {i}"), }); let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new( // Обратите внимание на завершающий пробел; большинство вызовов // `QueryBuilder` не добавляют пробелы автоматически, так как это может // помешать идентификаторам или строкам с кавычками, где могут быть // важны точные значения. "SELECT * FROM users WHERE (id, username, email, password) in" ); // Отмечу, что .into_iter() здесь не понадобилось, поскольку users // уже является итератором. Если бы вы хотели связать их по ссылке, // а не по значению, вам бы понадобился итератор, который бы выдавал // ссылки, живущие столько же, сколько и `query_builder`, для чего, // например, можно было бы сначала собрать их в `Vec`. bound.push_bind(foo.id) .push_bind(foo.username); }); let mut query = query_builder.build(); let res = query.fetch_all(&pool).await.unwrap();
Если вы попробуете выполнить этот запрос, то вы получите вектор структур Foo! Однако следует помнить, что этот метод имеет свои недостатки, о которых будет сказано ниже: вы не сможете воспользоваться макросами проверки времени компиляции SQLx, а также этот метод генерации запросов может быть несколько небезопасным, если вы не будете проявлять особую осторожность. Однако в рамках задач, предполагающих динамическую генерацию запросов с использованием SQL в Rust, этот метод является достаточно мощным.
Последний тип запроса, который мы также можем использовать, — это скалярный подзапрос, который возвращает результат в виде кортежа. Если при выполнении запроса (например) SELECT * FROM TABLE мы не знаем, сколько в нем полей, мы можем использовать query_scalar, что позволит нам ссылаться на столбцы просто по порядку их следования, а не по заданному имени. Смотрите пример ниже:
let query = sqlx::query_scalar("SELECT * FROM FOO LIMIT 1").fetch_one(&pool).await.unwrap(); println!("{:?}", query.0);
Макросы
Мы наконец подошли к одному из достоинств SQLx: проверке запросов во время компиляции. Если вы используете чистый (raw) SQL, наличие некоторой гарантии того, что ваш SQL корректен, практически никогда не будет лишним: если вы не администратор базы данных, то при выполнении запроса с несколькими джоинами вам обязательно захочется убедиться в его корректности до того, как он будет запущен. Следует отметить, что для использования этой возможности необходимо установить sqlx-cli: если его нет, то вам придется полагаться только на предыдущие методы.
Простой запрос с использованием макроса query! может выглядеть следующим образом:
// обратите внимание, что в макрос query добавляются связанные параметры let query = query!("SELECT * FROM FOO WHERE ID = $1", 1).fetch_one(&pool).await.unwrap();
Аналогично, эквивалентный запрос, использующий созданную ранее структуру Foo, может быть использован для прямого связывания результатов с вектором структур:
#[derive(sqlx::FromRow)] struct Foo { id: i32, message: String } let query = query_as!(Foo, "SELECT * FROM FOO").fetch_all(&pool).await.unwrap();
При использовании макроса query! или query_as! необходимо использовать программу cargo sqlx prepare, которая будет генерировать JSON-файлы для ваших запросов. Когда вы компилируете свою программу, она будет автоматически проверять ее во время компиляции: если что-то не так, она автоматически проверит это за вас.
Существует одна особенность, которая может поставить вас в тупик при использовании макросов проверки во время компиляции, особенно в Postgres: если вы используете as _ для переименования полей SQL, то тип будет автоматически обернут в Option, если вы не зададите его явно как ненулевое (non-nullable) значение. В SQLx есть ответ на этот вопрос — возможность использования чистых строк (raw strings) для явного объявления значений в качестве ненулевых столбцов. Например, возьмем следующий оператор:
let query = query_as!(Foo, "SELECT id, message as message from foo").fetch_all(&pool).await.unwrap();
Если бы у нас по-прежнему String был в качестве типа Message, то этот запрос не скомпилировался бы, поскольку message теперь является типом Option<String>, а не String. Однако, преобразовав приведенный выше запрос в чистую строку, мы можем заставить поле снова стать ненулевым:
// // обратите внимание, что теперь message —— это "message!" let query = query_as!(Foo, r#"SELECT id, message as "message!" from foo"#).fetch_all(&pool).await.unwrap();
Подробнее об этом можно прочитать здесь.
Подобным образом, конечно, query_scalar также имеет связанный макрос, который можно использовать аналогично макросу query!, возвращая при этом кортежи.
Еще одна замечательная возможность — хранить SQL-запрос в файле и запускать макрос для выполнения содержимого SQL-файла, сохраняя при этом привязку наших параметров. Смотрите пример ниже:
SELECT * FROM FOO WHERE id = $1;
let query = query_file!("query.sql", 1i32).fetch_one(&pool).await.unwrap();
Разумеется, этот макрос также поддерживает связывание со структурами и скалярные подзапросы с помощью query_file_as! и query_file_scalar!.
Следует отметить, что если вам требуется только проверка синтаксиса во время компиляции, а корректность входов и выходов базы данных для макроса запроса не важна, то в конце макроса можно добавить unchecked. Например: query! превратится в query_unchecked!. Это полезно в тех случаях, когда база данных еще не настроена или нет удобного способа получения URL базы данных (или в других подобных случаях, когда вы не хотите предоставлять SQLx прямой доступ к вашей базе данных).
Listen/Notify PostgreSQL
При таком количестве фич, как у Postgres, впечатляет, что SQLx способен их поддерживать — хотя SQLx в первую очередь предназначен для написания чистого SQL, нет каких-либо ограничений, заставляющих нас писать все только на нем. SQLx поддерживает каналы, LISTEN и, что еще важнее, pg_notify, который является для нас отличным способом обработки уведомлений от Postgres при обновлении записей. Давайте посмотрим на примере ниже, как мы можем настроить слушатель событий:
// заранее подготавливаем пул let mut listener = PgListener::connect_with(&pool).await.unwrap(); listener.listen("testNotify").await.unwrap(); // создаем цикл для получения уведомлений tokio::spawn(async move || { while let Some(notification) = listener.try_recv().await.unwrap() { println!("{notification:?}"); } }); loop { sqlx::query("SELECT pg_notify('testNotify', 'Hello world!')").execute(&pool).await; }
Как вы можете видеть здесь, мы породили задачу Tokio для асинхронного цикла получения уведомлений и их последующей печати — тем временем внутри основного потока выполнения мы также создали цикл для непрерывной отправки запроса «Hello world!» по каналу, который получает наш PgListener.
Для более продвинутой реализации в веб-сервисе, реализующем в качестве конечной точки поток изменений базы данных, следует использовать метод .into_stream(), поскольку фреймворки обычно принимают поток данных, который затем оборачивается в соответствующий тип из фреймворка. Например, в Axum вы бы использовали тип axum::response::Sse (обратите внимание, что это предполагает, что у вас уже есть настроенный веб-сервис):
use axum::{Extension, response::{Sse, sse::Event}}; use tokio_stream::StreamExt as _ ; use futures_util::stream::{self, Stream}; use std::convert::Infallbile; async fn return_stream(Extension(listener): Extension<PgListener>) -> Sse<impl Stream<Item = Result<Event, Infallible>>> { let stream = listener.into_stream(); Sse::new(stream .map(|msg| { let msg = msg.uwnrap(); let json = json!(msg).to_string(); Event::default().data(json) }).map(Ok), ).keep_alive(KeepAlive::default()) }
При настройке веб-сервиса мы можем создавать уведомления одним из двух способов:
-
Используя SQL
-
Используя
pg_notifyдля определенных событий
Сама работа с pg_notify довольно проста, хотя тут можно обойтись и без SQL, просто используя каналы Tokio. Давайте поднимемся на ступеньку выше и используем SQL для настройки наших каналов, чтобы нам не пришлось вручную генерировать их в коде.
CREATE TABLE IF NOT EXISTS test_table ( id SERIAL PRIMARY KEY, message TEXT NOT NULL ); CREATE TRIGGER "testNotify" AFTER INSERT ON test_table FOR EACH ROW EXECUTE PROCEDURE testNotify(); CREATE OR REPLACE FUNCTION testNotify() RETURNS TRIGGER AS $$ DECLARE BEGIN PERFORM pg_notify('testNotify', ROW_TO_JSON(NEW)::text); RETURN NEW; END; $$ LANGUAGE plpgsql;
Теперь, если мы добавим это в файл миграции SQL, затем запустим приложение и обратимся к конечной точке, которую мы используем для нашего потока, то мы сможем получать поток уведомлений!
Использование SQLx вместе с Shuttle
В настоящее время Shuttle предлагает SQLx в качестве соединения по умолчанию через наши аннотационные макросы, которые экономят время, позволяя создавать инфраструктуру прямо из кода. Все, что вам нужно сделать, это объявить макрос в коде, как, например, здесь:
use sqlx::PgPool; #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres] db: PgPool // gets объявляется здесь ) -> shuttle_axum::ShuttleAxum { sqlx::migrate!().run(&db).await.map_err(|e| format!("Oh no! Migrations failed :( {e}"); ... the rest of your code }
Наши бесплатные базы данных предоставляются через общий сервер баз данных (при этом пользователи имеют отдельные базы данных для каждого приложения). Однако теперь мы предлагаем 100% изолированные базы данных AWS RDS в нашем плане Pro, о котором вы можете узнать здесь, с поддержкой MySQL, Postgres и MariaDB.
Пару слов в завершение
Спасибо, что прочитали эту статью! Я надеюсь, что вы получили хорошее представление о том, как можно использовать SQL в Rust, а также о том, насколько полезным инструментом является SQLx, когда нам нужно заставить мощь чистых, проверяемых во время компиляции SQL-запросов работать на вас при использовании SQL в Rust.
Если эта статья была для вас полезна, то не стесняйтесь поставить нам звезду на GitHub.
В заключение приглашаем всех Rust-разработчиков на ближайшие открытые уроки:
Многопоточность без страха, 8 ноября
Распределённый Ledger на Rust, 27 ноября
ссылка на оригинал статьи https://habr.com/ru/articles/771288/
Добавить комментарий