Добро пожаловать в главу 32 нашего курса по Rust, состоящего из 40 разделов. Эта глава посвящена интеграции с базами данных — ключевому аспекту разработки приложений, где данные играют центральную роль. Мы подробно разберём, как Rust позволяет взаимодействовать с различными типами баз данных, начиная с реляционных (SQL) и заканчивая нереляционными (NoSQL). Глава разделена на шесть частей, и в этом документе мы сосредоточимся на первом разделе: работе с SQL базами данных через библиотеки rusqlite
и diesel
. Ожидайте глубокое погружение в тему с примерами кода, комментариями, практическими советами и разбором нюансов.
rusqlite
и diesel
SQL (Structured Query Language) — это стандартный язык для работы с реляционными базами данных. В Rust интеграция с SQL позволяет создавать надёжные и производительные приложения, сохраняя преимущества языка, такие как безопасность памяти и отсутствие гонок данных. В этом разделе мы рассмотрим две популярные библиотеки: rusqlite
для работы с SQLite и diesel
как более мощный инструмент с поддержкой PostgreSQL, MySQL и SQLite. Мы начнём с основ, постепенно углубляясь в детали, чтобы вы могли уверенно применять эти инструменты в своих проектах.
Rust — это язык, который сочетает производительность C++ с безопасностью на уровне компиляции. Эти качества делают его идеальным для работы с базами данных, где важны как скорость, так и надёжность. Интеграция с базами данных позволяет приложениям хранить, извлекать и обрабатывать данные, будь то списки пользователей, записи транзакций или сложные аналитические запросы.
SQL базы данных, такие как SQLite, PostgreSQL или MySQL, используют таблицы для структурирования данных, что обеспечивает строгую схему и мощные возможности запросов. В Rust для работы с ними существуют специализированные библиотеки, которые мы и разберём:
rusqlite
: Простая библиотека для работы с SQLite — встраиваемой базой данных, не требующей отдельного сервера.diesel
: ORM-фреймворк (Object-Relational Mapping) с поддержкой PostgreSQL, MySQL и SQLite, обеспечивающий типобезопасность и продвинутые возможности.Но зачем использовать SQL в Rust? Вот ключевые преимущества:
rusqlite
rusqlite
?SQLite — это легковесная встраиваемая база данных, которая хранит все данные в одном файле. Она идеально подходит для небольших приложений, тестирования или встраиваемых систем, где не требуется отдельный сервер. Библиотека rusqlite
предоставляет Rust-интерфейс для работы с SQLite, позволяя выполнять SQL-запросы, управлять транзакциями и обрабатывать результаты.
rusqlite
Для начала добавьте зависимость в ваш
Cargo.toml
:
[dependencies]
rusqlite = "0.29.0"
После этого импортируйте библиотеку в коде:
use rusqlite::{Connection, Result};
Если вы хотите использовать дополнительные возможности, например, функции JSON, добавьте соответствующую фичу: rusqlite = { version = "0.29.0", features = ["bundled", "json"] }
. Фича bundled
встраивает SQLite в ваш проект, устраняя зависимость от системной библиотеки.
rusqlite
Давайте разберём базовые операции: создание базы данных, таблиц, вставка данных, запросы, обновление и удаление.
SQLite использует файл для хранения данных. Если файла нет, он создаётся автоматически:
fn main() -> Result<()> {
let conn = Connection::open("my_database.db")?;
println!("База данных успешно открыта!");
Ok(())
}
Метод Connection::open
возвращает Result
, что позволяет обработать ошибки, например, если файл недоступен из-за прав доступа.
Создадим таблицу users
с полями id
, name
и
age
:
conn.execute(
"CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER NOT NULL
)",
[],
)?;
Ключевое слово IF NOT EXISTS
предотвращает ошибку, если таблица уже существует. Второй аргумент — массив параметров, здесь пустой, так как запрос статичен.
Добавим пользователя "Alice" с возрастом 30:
conn.execute(
"INSERT INTO users (name, age) VALUES (?1, ?2)",
["Alice", 30],
)?;
Здесь используются placeholders
?1
и ?2
для безопасной передачи параметров, что защищает от SQL-инъекций. Параметры передаются как массив с типом, реализующим ToSql
.
Извлечём всех пользователей из таблицы:
#[derive(Debug)]
struct User {
id: i32,
name: String,
age: i32,
}
let mut stmt = conn.prepare("SELECT id, name, age FROM users")?;
let users_iter = stmt.query_map([], |row| {
Ok(User {
id: row.get(0)?,
name: row.get(1)?,
age: row.get(2)?,
})
})?;
for user in users_iter {
println!("Найден пользователь {:?}", user?);
}
Мы определили структуру User
с атрибутом #[derive(Debug)]
для вывода. Метод prepare
готовит запрос, а query_map
преобразует строки результата в экземпляры User
. Оператор ?
передаёт ошибки вверх.
Обновим возраст пользователя с id=1
:
conn.execute(
"UPDATE users SET age = ?1 WHERE id = ?2",
[35, 1],
)?;
Запрос обновляет только записи, соответствующие условию WHERE
.
Удалим пользователя с id=1
:
conn.execute(
"DELETE FROM users WHERE id = ?1",
[1],
)?;
Без условия WHERE
удалятся все записи, так что будьте осторожны!
rusqlite
Транзакции обеспечивают атомарность операций. Пример вставки двух пользователей:
let mut tx = conn.transaction()?;
tx.execute(
"INSERT INTO users (name, age) VALUES (?1, ?2)",
["Bob", 25]
)?;
tx.execute(
"INSERT INTO users (name, age) VALUES (?1, ?2)",
["Charlie", 28]
)?;
tx.commit()?;
Если произойдёт ошибка, вызовите tx.rollback()?
для отката изменений. Без commit
изменения не сохранятся.
Соберём всё вместе:
use rusqlite::{Connection, Result};
use std::error::Error;
#[derive(Debug)]
struct User {
id: i32,
name: String,
age: i32,
}
fn main() -> Result<(), Box> {
let conn = Connection::open("users.db")?;
conn.execute(
"CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER NOT NULL
)",
[],
)?;
conn.execute(
"INSERT INTO users (name, age) VALUES (?1, ?2)",
["Alice", 30]
)?;
conn.execute(
"INSERT INTO users (name, age) VALUES (?1, ?2)",
["Bob", 25]
)?;
let mut stmt = conn.prepare( "SELECT id, name, age FROM users" )?;
let users_iter = stmt.query_map([], |row| {
Ok(User {
id: row.get(0)?,
name: row.get(1)?,
age: row.get(2)?,
})
})?;
for user in users_iter {
println!("Найден пользователь {:?}", user?);
}
Ok(())
}
Этот код создаёт таблицу, добавляет данные и выводит их. Ошибки обрабатываются через Box<dyn Error>
для гибкости.
r2d2
с адаптером r2d2_sqlite
.log
или кастомных обработчиков.diesel
diesel
?diesel
— это ORM и query builder для Rust, поддерживающий PostgreSQL, MySQL и SQLite. Он обеспечивает типобезопасность, генерацию кода из схемы базы данных и мощный DSL для запросов.
Преимущества:
diesel
Добавьте в Cargo.toml
:
[dependencies]
diesel = { version = "2.0.0", features = ["postgres"] }
dotenvy = "0.15"
Установите CLI для миграций:
cargo install diesel_cli --no-default-features --features "postgres"
Для PostgreSQL требуется libpq-dev
(Linux) или аналог в вашей системе.
Создайте файл .env
:
DATABASE_URL=postgres://username:password@localhost/mydb
Инициализируйте проект:
diesel setup
Это создаёт папку migrations
и файл diesel.toml
.
Сгенерируйте миграцию:
diesel migration generate create_users
В migrations/.../up.sql
:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
age INTEGER NOT NULL
)
В migrations/.../down.sql
:
DROP TABLE users
Примените миграцию:
diesel migration run
Это создаёт файл src/schema.rs
.
diesel
use diesel::prelude::*;
use diesel::pg::PgConnection;
fn establish_connection() -> PgConnection {
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
PgConnection::establish(&database_url).expect("Ошибка подключения")
}
Создайте файл models.rs
:
use diesel::prelude::*;
use crate::schema::users;
#[derive(Queryable, Debug)]
pub struct User {
pub id: i32,
pub name: String,
pub age: i32,
}
#[derive(Insertable)]
[diesel(table_name = users)]
pub struct NewUser<'a> {
pub name: &'a str,
pub age: i32,
}
use crate::schema::users;
let new_user = NewUser { name: "Alice", age: 30 };
diesel::insert_into(users::table).values(&new_user).execute(&connection)?;
use crate::schema::users::dsl::*;
let results = users.load::<User>(&connection)?;
for user in results {
println!("{:?}", user);
}
// С фильтром
let filtered = users.filter(age.gt(25)).load::<User>(&connection)?;
diesel::update(users.filter(id.eq(1))).set(age.eq(35)).execute(&connection)?;
diesel::delete(users.filter(id.eq(1))).execute(&connection)?;
connection.transaction(|conn| {
diesel::insert_into(users::table).values(&new_user1).execute(conn)?;
diesel::insert_into(users::table).values(&new_user2).execute(conn)?;
Ok(())
})?;
use diesel::prelude::*;
use diesel::pg::PgConnection;
use std::env;
mod schema;
mod models;
use models::{User, NewUser};
use schema::users;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let connection = establish_connection();
let new_user = NewUser { name: "Alice", age: 30 };
diesel::insert_into(users::table).values(&new_user).execute(&connection)?;
let results = users::table.load::<User>(&connection)?;
for user in results {
println!("{:?}", user);
}
Ok(())
}
r2d2-diesel
в продакшене.diesel::debug_query
для отладки запросов.rusqlite
и diesel
Аспект | rusqlite |
diesel |
---|---|---|
Базы данных | Только SQLite | PostgreSQL, MySQL, SQLite |
Уровень абстракции | Низкий (ручные SQL-запросы) | Высокий (ORM, query builder) |
Типобезопасность | Ограниченная | Полная |
Сложность | Простая | Требует настройки |
Выбирайте rusqlite
для простоты и SQLite, а diesel
— для сложных проектов с типобезопасностью.
В этом разделе мы изучили основы работы с SQL в Rust через rusqlite
и diesel
. Вы теперь можете создавать таблицы, управлять данными и использовать транзакции. В следующих разделах мы рассмотрим NoSQL, CRUD, миграции и примеры.
В этом разделе мы углубимся в интеграцию языка программирования Rust с NoSQL базами данных, а именно с Redis и MongoDB, используя библиотеки redis-rs
и mongodb
. NoSQL базы данных отличаются от традиционных SQL баз своей гибкостью, масштабируемостью и подходом к хранению данных, что делает их идеальными для современных приложений, требующих высокой производительности и работы с неструктурированными данными. Мы разберем, как подключаться к этим базам, выполнять основные операции, рассмотрим примеры кода, лучшие практики и потенциальные "подводные камни".
NoSQL (Not Only SQL) базы данных — это класс систем управления базами данных, которые не используют строгую реляционную модель, характерную для SQL баз данных. Они предназначены для работы с неструктурированными или полуструктурированными данными и предлагают гибкость в структуре данных, высокую производительность и горизонтальную масштабируемость. В отличие от SQL баз, где данные организованы в таблицы с фиксированной схемой, NoSQL базы могут использовать различные модели данных:
В этом разделе мы сосредоточимся на двух популярных NoSQL базах: Redis (ключ-значение) и MongoDB (документоориентированная).
Redis — это высокопроизводительная база данных, работающая в оперативной памяти (in-memory), что обеспечивает молниеносный доступ к данным. Она поддерживает различные структуры данных, такие как строки, хеши, списки, множества и отсортированные множества. Redis часто используется для кэширования, управления сессиями, очередей сообщений и других задач, где важна скорость.
Для работы с Redis в Rust мы будем использовать библиотеку redis-rs
, которая предоставляет удобный интерфейс для синхронного и асинхронного взаимодействия с Redis.
Чтобы начать использовать redis-rs
, добавьте зависимость в ваш файл Cargo.toml
:
[dependencies]
redis = "0.21.0"
После этого выполните cargo build
, чтобы загрузить и скомпилировать библиотеку.
Давайте разберем основные операции с Redis, начиная с подключения к серверу и заканчивая работой с различными структурами данных.
Для взаимодействия с Redis сначала нужно установить соединение с сервером. Это делается с помощью структуры redis::Client
:
use redis::Client;
fn main() -> redis::RedisResult<()> {
let client = Client::open("redis://127.0.0.1/")?;
let mut con = client.get_connection()?;
println!("Подключение к Redis установлено!");
Ok(())
}
Здесь redis://127.0.0.1/
— это строка подключения, указывающая на локальный сервер Redis, работающий на порту по умолчанию (6379). Метод get_connection
возвращает объект соединения, который мы будем использовать для выполнения команд.
Внимание: Убедитесь, что сервер Redis запущен, иначе вы получите ошибку соединения. Запустить Redis можно командой redis-server
в терминале.
Простейшая операция в Redis — это установка и получение строковых значений с помощью команд SET
и GET
:
<(CRLF)code> use redis::Commands; fn main() -> redis::RedisResult<()> { let client = Client::open("redis://127.0.0.1/")?; let mut con = client.get_connection()?; // Установка значения con.set("my_key", "my_value")?; // Получение значения let value: String = con.get("my_key")?; println!("Значение: {}", value); Ok(()) }
Трейт redis::Commands
предоставляет методы для выполнения команд Redis. Тип возвращаемого значения (String
в данном случае) указывается явно, так как get
может возвращать разные типы данных в зависимости от контекста.
Хеши позволяют хранить несколько пар "поле-значение" под одним ключом. Это полезно, например, для представления объектов:
use redis::Commands;
fn main() -> redis::RedisResult<()> {
let client = Client::open("redis://127.0.0.1/")?;
let mut con = client.get_connection()?;
// Установка значений в хеше
con.hset("user:1", "name", "Alice")?;
con.hset("user:1", "age", "25")?;
// Получение одного значения
let name: String = con.hget("user:1", "name")?;
println!("Имя: {}", name);
// Получение всех значений хеша
let user: std::collections::HashMap<String, String> = con.hgetall("user:1")?;
for (field, value) in user {
println!("{}: {}", field, value);
}
Ok(())
}
Метод hset
добавляет поля в хеш, hget
извлекает конкретное значение, а hgetall
возвращает весь хеш как HashMap
.
Списки в Redis — это упорядоченные коллекции строк, которые можно использовать как очереди или стеки:
use redis::Commands;
fn main() -> redis::RedisResult<()> {
let client = Client::open("redis://127.0.0.1/")?;
let mut con = client.get_connection()?;
// Добавление элементов в список
con.lpush("tasks", "task1")?;
con.lpush("tasks", "task2")?;
// Получение всех элементов списка
let tasks: Vec<String> = con.lrange("tasks", 0, -1)?;
for task in tasks {
println!("Задача: {}", task);
}
Ok(())
}
lpush
добавляет элементы в начало списка, а lrange
извлекает элементы по заданному диапазону (от 0 до -1 означает весь список).
Для асинхронных приложений redis-rs
поддерживает работу с Tokio или async-std. Добавьте зависимости в Cargo.toml
:
[dependencies]
redis = { version = "0.21.0", features = ["tokio-comp"] }
tokio = { version = "1.0", features = ["full"] }
Пример асинхронного кода:
use redis::AsyncCommands;
use tokio;
#[tokio::main]
async fn main() -> redis::RedisResult<()> {
let client = redis::Client::open("redis://127.0.0.1/")?;
let mut con = client.get_async_connection().await?;
// Асинхронная установка и получение значения
con.set("async_key", "async_value").await?;
let value: String = con.get("async_key").await?;
println!("Значение: {}", value);
Ok(())
}
Метод get_async_connection
возвращает асинхронное соединение, а команды вызываются с использованием .await
.
redis::ConnectionManager
для управления пулом соединений, чтобы избежать накладных расходов на повторное создание соединений.RedisResult
, так как сетевые сбои или неверные команды могут привести к ошибкам.serde
для преобразования структур Rust в строки или бинарные данные.EXPIRE
для автоматического удаления устаревших данных: con.expire("key", 60)?
(время в секундах).MongoDB — это документоориентированная NoSQL база данных, которая хранит данные в формате BSON (Binary JSON). Она идеально подходит для приложений с гибкими схемами данных, где структура может меняться со временем. MongoDB поддерживает вложенные документы, массивы и мощные запросы.
Для работы с MongoDB в Rust используется библиотека mongodb
, предоставляющая асинхронный клиент (синхронный API не поддерживается).
Добавьте зависимость в Cargo.toml
:
[dependencies]
mongodb = "2.0.0"
tokio = { version = "1.0", features = ["full"] }
Библиотека требует аснхронный runtime, например Tokio, так как все операции асинхронные.
Давайте рассмотрим базовые операции CRUD (Create, Read, Update, Delete) с MongoDB.
Подключение осуществляется через mongodb::Client
:
use mongodb::Client;
#[tokio::main]
async fn main() -> mongodb::error::Result<()> {
let client = Client::with_uri_str("mongodb://localhost:27017/").await?;
let db = client.database("mydb");
println!("Подключение к MongoDB установлено!");
Ok(())
}
Строка mongodb://localhost:27017/
указывает на локальный сервер MongoDB. Метод database
возвращает объект базы данных.
Внимание: Убедитесь, что MongoDB запущена (mongod
), иначе подключение завершится ошибкой.
Для вставки данных используется метод insert_one
:
use mongodb::bson::doc;
use mongodb::Collection;
#[tokio::main]
async fn main() -> mongodb::error::Result<()> {
let client = Client::with_uri_str("mongodb://localhost:27017/").await?;
let db = client.database("mydb");
let collection: Collection<mongodb::bson::Document> = db.collection("users");
let doc = doc! { "name": "Bob", "age": 30 };
collection.insert_one(doc, None).await?;
println!("Документ вставлен!");
Ok(())
}
Макрос doc!
создает BSON-документ, который затем вставляется в коллекцию users
.
Для поиска используется метод find
, возвращающий курсор:
use mongodb::bson::doc;
use mongodb::Collection;
#[tokio::main]
async fn main() -> mongodb::error::Result<()> {
let client = Client::with_uri_str("mongodb://localhost:27017/").await?;
let db = client.database("mydb");
let collection: Collection<mongodb::bson::Document> = db.collection("users");
let filter = doc! { "name": "Bob" };
let mut cursor = collection.find(filter, None).await?;
while let Some(result) = cursor.next().await {
match result {
Ok(document) => println!("Найден документ: {:?}", document),
Err(e) => return Err(e),
}
}
Ok(())
}
Фильтр doc! { "name": "Bob" }
указывает, какие документы искать. Курсор позволяет итерироваться по результатам.
Для обновления используется update_one
:
use mongodb::bson::doc;
use mongodb::Collection;
#[tokio::main]
async fn main() -> mongodb::error::Result<()> {
let client = Client::with_uri_str("mongodb://localhost:27017/").await?;
let db = client.database("mydb");
let collection: Collection<mongodb::bson::Document> = db.collection("users");
let filter = doc! { "name": "Bob" };
let update = doc! { "$set": { "age": 31 } };
collection.update_one(filter, update, None).await?;
println!("Документ обновлен!");
Ok(())
}
Оператор $set
изменяет только указанные поля, оставляя остальную часть документа нетронутой.
Для удаления используется delete_one
:
use mongodb::bson::doc;
use mongodb::Collection;
#[tokio::main]
async fn main() -> mongodb::error::Result<()> {
let client = Client::with_uri_str("mongodb://localhost:27017/").await?;
let db = client.database("mydb");
let collection: Collection<mongodb::bson::Document> = db.collection("users");
let filter = doc! { "name": "Bob" };
collection.delete_one(filter, None).await?;
println!("Документ удален!");
Ok(())
}
collection.create_index(doc! { "name": 1 }, None).await?
.serde
для преобразования структур Rust в BSON и обратно, добавив #[derive(Serialize, Deserialize)]
.Result
, так как сетевые проблемы или неверные запросы могут вызвать сбои.Интеграция Rust с NoSQL базами данных, такими как Redis и MongoDB, открывает широкие возможности для создания быстрых и масштабируемых приложений. redis-rs
идеально подходит для задач, требующих высокой скорости (кэширование, очереди), а mongodb
— для работы с гибкими и сложными данными. Обе библиотеки предоставляют мощные инструменты для выполнения операций, но требуют внимания к обработке ошибок, управлению соединениями и оптимизации производительности.
В следующих разделах мы рассмотрим SQL базы данных, CRUD-операции, миграции, ORM и практические примеры. Оставайтесь с нами!
В этом разделе мы подробно разберем основы работы с базами данных через CRUD-операции: Create (создание), Read (чтение), Update (обновление) и Delete (удаление). Эти операции составляют фундамент взаимодействия с любыми базами данных, будь то реляционные SQL или документоориентированные NoSQL. Мы рассмотрим реализацию CRUD-операций в Rust на примере SQLite (с использованием библиотеки rusqlite
) для SQL и MongoDB (с использованием библиотеки mongodb
) для NoSQL. Вас ждут подробные объяснения, примеры кода, лучшие практики, а также разбор потенциальных проблем и нюансов, которые могут встретиться на пути.
CRUD — это акроним, который расшифровывается как:
Эти операции лежат в основе большинства приложений, работающих с данными. Их реализация может варьироваться в зависимости от типа базы данных (реляционная или нереляционная) и используемых инструментов. В Rust мы можем эффективно работать с обеими категориями баз данных благодаря мощным библиотекам.
SQLite — это легковесная реляционная база данных, которая не требует отдельного серверного процесса и хранит все данные в одном файле. Библиотека rusqlite
предоставляет удобный и безопасный интерфейс для работы с SQLite в Rust.
Для начала работы добавьте зависимость в ваш файл Cargo.toml
:
[dependencies]
rusqlite = "0.26.3"
После этого выполните команду cargo build
, чтобы скачать и скомпилировать библиотеку.
Первый шаг — установить соединение с базой данных. Если указанный файл базы данных не существует, SQLite создаст его автоматически.
use rusqlite::{Connection, Result};
fn main() -> Result<()> {
let conn = Connection::open("mydb.db")?;
println!("Подключение к SQLite установлено!");
Ok(())
}
Функция Connection::open("mydb.db")
открывает или создает файл базы данных с именем mydb.db
. Возвращаемый тип Result
позволяет обработать возможные ошибки, такие как проблемы с доступом к файлу.
Перед вставкой данных необходимо создать таблицу. Рассмотрим пример создания таблицы users
с полями id
, name
и age
.
use rusqlite::{Connection, Result};
fn main() -> Result<()> {
let conn = Connection::open("mydb.db")?;
// Создание таблицы, если она еще не существует
conn.execute(
"CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER NOT NULL
)",
[],
)?;
// Вставка данных
conn.execute(
"INSERT INTO users (name, age) VALUES (?1, ?2)",
["Alice", 25],
)?;
println!("Данные успешно вставлены!");
Ok(())
}
Метод execute
выполняет SQL-запрос. Плейсхолдеры ?1
и ?2
используются для безопасной передачи параметров, что предотвращает SQL-инъекции. Поле id
автоматически заполняется благодаря INTEGER PRIMARY KEY
.
Для чтения данных из базы можно использовать методы query_row
(для одной строки) или prepare
с query_map
(для нескольких строк).
use rusqlite::{Connection, Result};
fn main() -> Result<()> {
let conn = Connection::open("mydb.db")?;
// Чтение одной строки
let mut stmt = conn.prepare("SELECT id, name, age FROM users WHERE id = ?1")?;
let user = stmt.query_row([1], |row| {
Ok((
row.get::<_, i32>(0)?,
row.get::<_, String>(1)?,
row.get::<_, i32>(2)?,
))
})?;
println!("Найден пользователь: {:?}", user);
// Чтение нескольких строк
let mut stmt = conn.prepare("SELECT id, name, age FROM users")?;
let users_iter = stmt.query_map([], |row| {
Ok((
row.get::<_, i32>(0)?,
row.get::<_, String>(1)?,
row.get::<_, i32>(2)?,
))
})?;
for user in users_iter {
println!("Пользователь: {:?}", user?);
}
Ok(())
}
Метод query_row
возвращает кортеж с данными одной строки, а query_map
создает итератор по всем строкам результата запроса. Обратите внимание на использование row.get
с указанием типа данных для извлечения значений из столбцов.
Для обновления данных используется SQL-запрос UPDATE
с параметрами.
use rusqlite::{Connection, Result};
fn main() -> Result<()> {
let conn = Connection::open("mydb.db")?;
conn.execute(
"UPDATE users SET age = ?1 WHERE id = ?2",
[30, 1],
)?;
println!("Данные успешно обновлены!");
Ok(())
}
Этот запрос изменяет возраст пользователя с id = 1
на 30. Плейсхолдеры обеспечивают безопасность и читаемость кода.
Удаление данных выполняется с помощью SQL-запроса DELETE
.
use rusqlite::{Connection, Result};
fn main() -> Result<()> {
let conn = Connection::open("mydb.db")?;
conn.execute(
"DELETE FROM users WHERE id = ?1",
[1],
)?;
println!("Данные успешно удалены!");
Ok(())
}
Запрос удаляет пользователя с id = 1
. Если запись с таким id
не существует, операция завершится без ошибок, но ничего не удалит.
MongoDB — это документоориентированная NoSQL база данных, которая хранит данные в формате BSON (бинарный JSON). В отличие от SQL, здесь нет строгой схемы, а данные могут быть вложенными и гибкими по структуре.
Для работы с MongoDB в Rust используется библиотека mongodb
. Добавьте зависимости в Cargo.toml
:
[dependencies]
mongodb = "2.0.0"
tokio = { version = "1.0", features = ["full"] }
Библиотека mongodb
работает асинхронно и требует runtime, такой как Tokio.
Подключение осуществляется через mongodb::Client
с использованием строки подключения.
use mongodb::Client;
#[tokio::main]
async fn main() -> mongodb::error::Result<()> {
let client = Client::with_uri_str("mongodb://localhost:27017/").await?;
let db = client.database("mydb");
println!("Подключение к MongoDB установлено!");
Ok(())
}
Строка mongodb://localhost:27017/
указывает на локальный сервер MongoDB. Метод database
выбирает базу данных mydb
.
Для вставки данных используется метод insert_one
.
use mongodb::bson::doc;
use mongodb::Collection;
#[tokio::main]
async fn main() -> mongodb::error::Result<()> {
let client = Client::with_uri_str("mongodb://localhost:27017/").await?;
let db = client.database("mydb");
let collection: Collection<mongodb::bson::Document> = db.collection("users");
let doc = doc! { "name": "Alice", "age": 25 };
collection.insert_one(doc, None).await?;
println!("Документ успешно вставлен!");
Ok(())
}
Макрос doc!
создает документ в формате BSON, который затем вставляется в коллекцию users
. Поле _id
автоматически добавляется MongoDB, если не указано явно.
Для чтения данных используется метод find
, который возвращает асинхронный курсор.
use mongodb::bson::doc;
use mongodb::Collection;
#[tokio::main]
async fn main() -> mongodb::error::Result<()> {
let client = Client::with_uri_str("mongodb://localhost:27017/").await?;
let db = client.database("mydb");
let collection: Collection<mongodb::bson::Document> = db.collection("users");
let filter = doc! { "name": "Alice" };
let mut cursor = collection.find(filter, None).await?;
while let Some(result) = cursor.next().await {
match result {
Ok(document) => println!("Найден документ: {:?}", document),
Err(e) => return Err(e),
}
}
Ok(())
}
Фильтр doc! { "name": "Alice" }
указывает, какие документы нужно найти. Курсор позволяет итерироваться по всем подходящим документам.
Для обновления используется метод update_one
.
use mongodb::bson::doc;
use mongodb::Collection;
#[tokio::main]
async fn main() -> mongodb::error::Result<()> {
let client = Client::with_uri_str("mongodb://localhost:27017/").await?;
let db = client.database("mydb");
let collection: Collection<mongodb::bson::Document> = db.collection("users");
let filter = doc! { "name": "Alice" };
let update = doc! { "$set": { "age": 26 } };
collection.update_one(filter, update, None).await?;
println!("Документ успешно обновлен!");
Ok(())
}
Оператор $set
обновляет только указанные поля, оставляя остальную часть документа неизменной.
Для удаления используется метод delete_one
.
use mongodb::bson::doc;
use mongodb::Collection;
#[tokio::main]
async fn main() -> mongodb::error::Result<()> {
let client = Client::with_uri_str("mongodb://localhost:27017/").await?;
let db = client.database("mydb");
let collection: Collection<mongodb::bson::Document> = db.collection("users");
let filter = doc! { "name": "Alice" };
collection.delete_one(filter, None).await?;
println!("Документ успешно удален!");
Ok(())
}
Этот запрос удаляет первый документ, соответствующий фильтру {"name": "Alice"}
.
Result
или Option
. Например, отсутствие соединения или неверный запрос могут привести к сбоям, если ошибки не обработаны.rusqlite
используйте плейсхолдеры (?1
, ?2
), чтобы избежать SQL-инъекций. В MongoDB фильтры также должны быть корректно сформированы.CREATE INDEX idx_name ON users(name)
). В MongoDB используйте create_index
для повышения производительности.conn.transaction()
) для атомарности нескольких операций. Например:
let tx = conn.transaction()?;
tx.execute("INSERT INTO users (name, age) VALUES (?1, ?2)", ["Bob", 22])?;
tx.commit()?;
Заметка: В MongoDB можно использовать insert_many
или update_many
для работы с несколькими документами одновременно, что полезно для пакетной обработки данных.
Предупреждение: SQLite не поддерживает высокую конкурентность записи, поэтому для многопоточных приложений рассмотрите другие базы данных, такие как PostgreSQL.
CRUD-операции — это основа взаимодействия с базами данных в любом приложении. В этом разделе мы подробно рассмотрели, как выполнять эти операции в Rust, используя SQLite для SQL и MongoDB для NoSQL. Вы узнали, как подключаться к базам данных, создавать, читать, обновлять и удалять данные, а также как избегать распространенных ошибок. Эти знания помогут вам строить надежные и эффективные приложения, независимо от типа используемой базы данных.
В следующих разделах мы разберем миграции, ORM, практические примеры (например, базу данных задач) и упражнение по созданию списка дел. Продолжайте изучение!
В этом разделе мы углубимся в темы миграций и объектно-реляционного отображения (ORM) при работе с базами данных в Rust. Миграции позволяют управлять изменениями схемы базы данных, а ORM упрощает взаимодействие с базой данных, предоставляя объектно-ориентированный интерфейс. Мы сосредоточимся на библиотеке diesel
, которая является одной из самых популярных и мощных ORM для Rust, поддерживающей PostgreSQL, MySQL и SQLite. Вы узнаете, как настраивать миграции, создавать модели, выполнять запросы и управлять схемой базы данных. Также мы рассмотрим лучшие практики, потенциальные "подводные камни" и нюансы работы с diesel
.
В Rust библиотека diesel
предоставляет как ORM, так и инструменты для управления миграциями, что делает её отличным выбором для работы с SQL базами данных.
Для начала работы с diesel
необходимо установить CLI-инструмент и добавить зависимости в проект.
Diesel CLI — это утилита командной строки, которая помогает управлять миграциями и генерировать схему. Установите её с помощью Cargo:
cargo install diesel_cli --no-default-features --features "postgres"
В этом примере мы устанавливаем Diesel CLI с поддержкой PostgreSQL. Для MySQL или SQLite замените "postgres"
на "mysql"
или "sqlite"
соответственно.
Заметка: Для SQLite также потребуется установить библиотеку libsqlite3-dev
(или аналогичную для вашей ОС), чтобы Cargo мог скомпилировать rusqlite
.
Добавьте зависимости в ваш файл Cargo.toml
. Для примера с PostgreSQL:
[dependencies]
diesel = { version = "1.4.8", features = ["postgres", "r2d2"] }
dotenv = "0.15.0"
Здесь r2d2
— это пул соединений, а dotenv
поможет управлять переменными окружения, такими как строка подключения к базе данных.
Создайте файл .env
в корне проекта с переменной DATABASE_URL
, указывающей на вашу базу данных. Например, для PostgreSQL:
DATABASE_URL=postgres://username:password@localhost/mydb
Затем выполните команду для инициализации Diesel:
diesel setup
Эта команда создаст директорию migrations
для хранения миграций и файл diesel.toml
с конфигурацией.
Миграции в Diesel представляют собой пары SQL-файлов: up.sql
(для применения изменений) и down.sql
(для отката изменений). Давайте создадим миграцию для таблицы users
.
Выполните команду:
diesel migration generate create_users
Это создаст новую директорию в migrations
с файлами up.sql
и down.sql
. Отредактируйте up.sql
:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
age INTEGER NOT NULL
);
И down.sql
:
DROP TABLE users;
Для применения миграции выполните:
diesel migration run
Это выполнит up.sql
и создаст таблицу users
. Если нужно откатить миграцию, используйте diesel migration redo
, что выполнит down.sql
, а затем снова up.sql
.
Diesel позволяет определять модели, которые соответствуют таблицам в базе данных, и выполнять типобезопасные запросы.
Сначала сгенерируйте схему базы данных. После применения миграции выполните:
diesel print-schema > src/schema.rs
Это создаст файл schema.rs
с определением схемы. Теперь определите модель User
в src/models.rs
:
use diesel::prelude::*;
use crate::schema::users;
#[derive(Queryable, Insertable)]
#[table_name="users"]
pub struct User {
pub id: i32,
pub name: String,
pub age: i32,
}
Атрибуты #[derive(Queryable, Insertable)]
позволяют использовать эту структуру для запросов и вставки данных. #[table_name="users"]
связывает структуру с таблицей users
.
Для подключения к базе данных используйте diesel::PgConnection
(или аналог для другой СУБД). Рекомендуется использовать пул соединений, например, r2d2
.
use diesel::r2d2::{ConnectionManager, Pool};
use dotenv::dotenv;
use std::env;
pub type DbPool = Pool>;
pub fn establish_connection() -> DbPool {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let manager = ConnectionManager::::new(database_url);
Pool::builder().build(manager).expect("Failed to create pool")
}
Этот код создает пул соединений, который можно использовать в приложении для выполнения запросов.
Давайте рассмотрим, как выполнять CRUD-операции с использованием моделей и запросов Diesel.
Для вставки нового пользователя:
use diesel::prelude::*;
use crate::models::User;
use crate::schema::users;
fn create_user(conn: &PgConnection, name: &str, age: i32) -> Result {
let new_user = User { id: 0, name: name.to_string(), age }; // id будет сгенерирован базой
diesel::insert_into(users::table)
.values(&new_user)
.get_result(conn)
}
Метод insert_into
выполняет вставку, а get_result
возвращает вставленную запись (с сгенерированным id
).
Для чтения всех пользователей:
use diesel::prelude::*;
use crate::models::User;
use crate::schema::users::dsl::*;
fn get_users(conn: &PgConnection) -> Result, diesel::result::Error> {
users.load::(conn)
}
Для чтения с фильтром, например, по имени:
fn get_user_by_name(conn: &PgConnection, user_name: &str) -> Result {
users.filter(name.eq(user_name)).first(conn)
}
Метод filter
добавляет условие WHERE, а first
возвращает первую запись или ошибку, если запись не найдена.
Для обновления возраста пользователя по id
:
use diesel::prelude::*;
use crate::schema::users::dsl::*;
fn update_user_age(conn: &PgConnection, user_id: i32, new_age: i32) -> Result {
diesel::update(users.find(user_id))
.set(age.eq(new_age))
.execute(conn)
}
Метод update
с find
обновляет запись по первичному ключу. Возвращаемое значение — количество обновленных строк.
Для удаления пользователя по id
:
use diesel::prelude::*;
use crate::schema::users::dsl::*;
fn delete_user(conn: &PgConnection, user_id: i32) -> Result {
diesel::delete(users.find(user_id)).execute(conn)
}
Метод delete
удаляет запись и возвращает количество удаленных строк.
Result
, поэтому всегда обрабатывайте возможные ошибки, такие как отсутствие соединения или нарушение ограничений.r2d2
) для управления ресурсами и повышения производительности в многопоточных приложениях.schema.rs
после изменения миграций с помощью diesel print-schema
.let mut conn = pool.get()?;
conn.transaction(|| {
// несколько операций
})?;
Предупреждение: Diesel не поддерживает автоматическую генерацию миграций на основе моделей. Вы должны вручную писать SQL для миграций, что требует хорошего знания SQL.
Миграции и ORM — это мощные инструменты для управления базами данных в Rust. С помощью diesel
вы можете эффективно управлять схемой базы данных, выполнять типобезопасные запросы и работать с данными через объектно-ориентированный интерфейс. В этом разделе мы рассмотрели, как настраивать миграции, создавать модели, выполнять CRUD-операции и избегать распространенных ошибок. Эти знания помогут вам строить надежные и масштабируемые приложения с использованием SQL баз данных.
В следующих разделах мы рассмотрим примеры баз данных задач и упражнение по созданию списка дел с базой данных. Продолжайте изучение!
В этом разделе мы рассмотрим практические примеры интеграции Rust с базами данных на примере базы данных задач (To-Do List). Мы реализуем простое приложение для управления задачами, используя как SQL (SQLite с библиотекой rusqlite
), так и NoSQL (MongoDB с библиотекой mongodb
). Вы увидите, как выполнять CRUD-операции, управлять соединениями и обрабатывать данные в обоих типах баз данных. Этот раздел поможет вам понять различия и особенности работы с реляционными и нереляционными базами данных в Rust.
SQLite — это легковесная реляционная база данных, идеально подходящая для небольших приложений или прототипов. Мы создадим таблицу tasks
с полями id
, description
и completed
, а затем реализуем функции для добавления, чтения, обновления и удаления задач.
Добавьте зависимость rusqlite
в ваш Cargo.toml
:
[dependencies]
rusqlite = "0.26.3"
Сначала создадим базу данных и таблицу tasks
, если она еще не существует.
use rusqlite::{Connection, Result};
fn init_db() -> Result<Connection> {
let conn = Connection::open("tasks.db")?;
conn.execute(
"CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY,
description TEXT NOT NULL,
completed BOOLEAN NOT NULL DEFAULT 0
)",
[],
)?;
Ok(conn)
}
Функция init_db
открывает соединение с базой данных tasks.db
и создает таблицу tasks
, если она не существует. Поле completed
типа BOOLEAN
хранит статус завершения задачи (0 — не завершена, 1 — завершена).
Функция для добавления новой задачи:
fn add_task(conn: &Connection, description: &str) -> Result<()> {
conn.execute(
"INSERT INTO tasks (description) VALUES (?1)",
[description],
)?;
Ok(())
}
Мы используем параметризованный запрос для безопасной вставки описания задачи.
Функция для получения всех задач:
fn get_tasks(conn: &Connection) -> Result<Vec<(i32, String, bool)>> {
let mut stmt = conn.prepare("SELECT id, description, completed FROM tasks")?;
let tasks_iter = stmt.query_map([], |row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
))
})?;
let mut tasks = Vec::new();
for task in tasks_iter {
tasks.push(task?);
}
Ok(tasks)
}
Мы используем query_map
для извлечения всех строк из таблицы и преобразования их в кортежи (id, description, completed)
.
Функция для обновления статуса завершения задачи по id
:
fn complete_task(conn: &Connection, id: i32) -> Result<()> {
conn.execute(
"UPDATE tasks SET completed = 1 WHERE id = ?1",
[id],
)?;
Ok(())
}
Функция для удаления задачи по id
:
fn delete_task(conn: &Connection, id: i32) -> Result<()> {
conn.execute(
"DELETE FROM tasks WHERE id = ?1",
[id],
)?;
Ok(())
}
Теперь соберем все вместе в функции main
:
fn main() -> Result<()> {
let conn = init_db()?;
// Добавляем задачи
add_task(&conn, "Купить молоко")?;
add_task(&conn, "Позвонить другу")?;
// Получаем и выводим все задачи
let tasks = get_tasks(&conn)?;
for task in tasks {
println!("ID: {}, Описание: {}, Завершена: {}", task.0, task.1, task.2);
}
// Завершаем задачу с id=1
complete_task(&conn, 1)?;
// Удаляем задачу с id=2
delete_task(&conn, 2)?;
Ok(())
}
Этот пример демонстрирует базовые CRUD-операции с SQLite. Вы можете расширить его, добавив, например, фильтрацию задач по статусу или дате.
MongoDB — это документоориентированная NoSQL база данных, которая хранит данные в гибком формате, похожем на JSON. Мы создадим коллекцию tasks
и реализуем те же CRUD-операции, что и в примере с SQLite.
Добавьте зависимости в Cargo.toml
:
[dependencies]
mongodb = "2.0.0"
tokio = { version = "1.0", features = ["full"] }
Поскольку mongodb
работает асинхронно, мы используем Tokio как runtime.
Установим соединение с MongoDB и выберем базу данных и коллекцию.
use mongodb::{Client, Collection};
use mongodb::bson::doc;
#[tokio::main]
async fn main() -> mongodb::error::Result<()> {
let client = Client::with_uri_str("mongodb://localhost:27017/").await?;
let db = client.database("tasks_db");
let collection: Collection<mongodb::bson::Document> = db.collection("tasks");
// Дальнейшие операции...
Ok(())
}
Для добавления новой задачи используем insert_one
:
async fn add_task(collection: &Collection<mongodb::bson::Document>, description: &str) -> mongodb::error::Result<()> {
let doc = doc! { "description": description, "completed": false };
collection.insert_one(doc, None).await?;
Ok(())
}
Мы создаем документ с полями description
и completed
и вставляем его в коллекцию.
Для получения всех задач используем find
:
async fn get_tasks(collection: &Collection<mongodb::bson::Document>) -> mongodb::error::Result<Vec<mongodb::bson::Document>> {
let mut cursor = collection.find(None, None).await?;
let mut tasks = Vec::new();
while let Some(result) = cursor.next().await {
tasks.push(result?);
}
Ok(tasks)
}
Метод find
без фильтра возвращает все документы в коллекции. Мы собираем их в вектор для дальнейшего использования.
Для обновления статуса задачи по _id
используем update_one
. Поскольку _id
в MongoDB является уникальным идентификатором, нам нужно его знать. Для простоты, предположим, что мы знаем _id
задачи.
use mongodb::bson::oid::ObjectId;
async fn complete_task(collection: &Collection<mongodb::bson::Document>, id: &str) -> mongodb::error::Result<()> {
let obj_id = ObjectId::with_string(id)?;
let filter = doc! { "_id": obj_id };
let update = doc! { "$set": { "completed": true } };
collection.update_one(filter, update, None).await?;
Ok(())
}
Здесь ObjectId::with_string
преобразует строковый id
в ObjectId
, который используется в MongoDB.
Для удаления задачи по _id
используем delete_one
:
async fn delete_task(collection: &Collection<mongodb::bson::Document>, id: &str) -> mongodb::error::Result<()> {
let obj_id = ObjectId::with_string(id)?;
let filter = doc! { "_id": obj_id };
collection.delete_one(filter, None).await?;
Ok(())
}
Соберем все вместе в асинхронной функции main
. Для упрощения, мы будем использовать фиктивные _id
, но в реальном приложении вы бы получали их из базы данных.
#[tokio::main]
async fn main() -> mongodb::error::Result<()> {
let client = Client::with_uri_str("mongodb://localhost:27017/").await?;
let db = client.database("tasks_db");
let collection: Collection<mongodb::bson::Document> = db.collection("tasks");
// Добавляем задачи
add_task(&collection, "Купить молоко").await?;
add_task(&collection, "Позвонить другу").await?;
// Получаем и выводим все задачи
let tasks = get_tasks(&collection).await?;
for task in tasks {
println!("Задача: {:?}", task);
}
// Предположим, что у нас есть _id задачи, например, "507f1f77bcf86cd799439011"
let task_id = "507f1f77bcf86cd799439011";
complete_task(&collection, task_id).await?;
// Удаляем задачу по _id
delete_task(&collection, task_id).await?;
Ok(())
}
Этот пример показывает, как выполнять CRUD-операции с MongoDB в Rust. Обратите внимание на асинхронный характер операций и необходимость управления _id
.
id
генерируется автоматически как целое число, в MongoDB _id
— это ObjectId
, который нужно обрабатывать отдельно.rusqlite
используют SQL-синтаксис, в то время как в MongoDB запросы строятся с помощью BSON-документов.rusqlite
работает синхронно, а mongodb
— асинхронно, что требует использования runtime, такого как Tokio.Выбор между SQL и NoSQL зависит от требований вашего приложения. SQL базы данных лучше подходят для структурированных данных с четкими отношениями, в то время как NoSQL — для гибких, неструктурированных данных и горизонтального масштабирования.
Result
или Option
. В MongoDB, например, ObjectId::with_string
может вернуть ошибку, если строка не является валидным ObjectId
.CREATE INDEX idx_description ON tasks(description)
. В MongoDB: collection.create_index(doc! { "description": 1 }, None).await?
.Заметка: В MongoDB можно использовать insert_many
для пакетной вставки нескольких задач, что может быть более эффективно.
Предупреждение: В SQLite одновременная запись из нескольких потоков может привести к блокировкам. Для высоконагруженных приложений рассмотрите другие базы данных, такие как PostgreSQL.
В этом разделе мы рассмотрели практические примеры интеграции Rust с базами данных на примере базы данных задач. Вы увидели, как реализовать CRUD-операции как в SQL (SQLite), так и в NoSQL (MongoDB), и узнали о различиях между этими подходами. Эти знания помогут вам выбрать подходящую базу данных для вашего приложения и эффективно работать с ней в Rust.
В следующем разделе мы предложим упражнение по созданию списка дел с базой данных, где вы сможете закрепить полученные знания на практике. Продолжайте изучение!
В этом упражнении вы научитесь интегрировать базу данных в ваше Rust-приложение на примере простого списка дел. Вы создадите консольное приложение, которое позволяет управлять задачами, хранящимися в базе данных SQLite. Мы будем использовать crate rusqlite
для работы с SQLite, так как это лёгкий и популярный выбор для начинающих, не требующий внешнего сервера базы данных. Цель — реализовать полный цикл CRUD-операций (создание, чтение, обновление, удаление) и предоставить пользователю удобный интерфейс через командную строку.
Сначала создайте новый проект Rust с помощью Cargo. Откройте терминал и выполните следующие команды:
cargo new todo_list
cd todo_list
Эти команды создадут новый проект с именем todo_list
и перейдут в его директорию. Далее необходимо добавить зависимость от crate rusqlite
в файл Cargo.toml
, чтобы ваш проект мог взаимодействовать с SQLite:
[dependencies]
rusqlite = "0.25.3"
После добавления зависимости Cargo автоматически загрузит и подключит rusqlite
при следующей сборке проекта. Указанная версия (0.25.3) проверена и стабильна на момент написания, но вы можете проверить crates.io для использования более новой версии, если она доступна.
Совет: Если вы хотите добавить дополнительные возможности, такие как поддержка типов времени или JSON, изучите документацию rusqlite
для подключения соответствующих feature-флагов в Cargo.toml
, например, rusqlite = { version = "0.25.3", features = ["chrono"] }
.
Ваше приложение будет хранить задачи в таблице базы данных. Каждая задача должна иметь уникальный идентификатор (id
), описание (description
) и статус выполнения (done
). Мы определим функцию для создания такой таблицы в SQLite:
use rusqlite::{Connection, Result};
fn create_table(conn: &Connection) -> Result<()> {
conn.execute(
"CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY,
description TEXT NOT NULL,
done BOOLEAN NOT NULL DEFAULT 0
)",
[],
)?;
Ok(())
}
Разберём этот код:
use rusqlite::{Connection, Result}
импортирует необходимые типы из rusqlite
. Connection
представляет соединение с базой данных, а Result
— тип результата, который обрабатывает ошибки SQLite.CREATE TABLE IF NOT EXISTS
создаёт таблицу tasks
, только если она ещё не существует, что предотвращает ошибки при повторном запуске программы.id INTEGER PRIMARY KEY
— уникальный идентификатор задачи, автоматически увеличивающийся благодаря SQLite.description TEXT NOT NULL
— текстовое описание задачи, обязательное поле.done BOOLEAN NOT NULL DEFAULT 0
— статус задачи (0 — не выполнена, 1 — выполнена), по умолчанию не выполнена.[]
— пустой массив параметров, так как запрос не использует подстановку значений.?
— оператор обработки ошибок из Rust, который возвращает ошибку, если операция не удалась.Подводный камень: SQLite не имеет настоящего типа BOOLEAN
— он интерпретирует его как INTEGER
(0 или 1). Убедитесь, что вы учитываете это при работе с данными.
Теперь реализуем функцию для добавления новой задачи в таблицу:
fn add_task(conn: &Connection, description: &str) -> Result<()> {
conn.execute(
"INSERT INTO tasks (description) VALUES (?1)",
[description],
)?;
Ok(())
}
Что здесь происходит:
description: &str
— аргумент функции, строка с описанием задачи.INSERT INTO tasks (description) VALUES (?1)
— SQL-запрос, который вставляет описание в столбец description
. ?1
— placeholder для безопасной подстановки значения.[description]
— массив параметров, где description
подставляется вместо ?1
.Лучшая практика: Всегда используйте параметризованные запросы (как ?1
), чтобы избежать SQL-инъекций. Например, если пользователь введёт "DROP TABLE tasks; --"
, параметризация предотвратит выполнение этого как команды.
Для отображения всех задач в списке реализуем функцию, которая возвращает их в виде вектора:
use rusqlite::Row;
fn list_tasks(conn: &Connection) -> Result<Vec<(i32, String, bool)>> {
let mut stmt = conn.prepare("SELECT id, description, done FROM tasks")?;
let tasks = stmt.query_map([], |row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
))
})?.collect();
tasks
}
Пошаговый разбор:
conn.prepare
подготавливает SQL-запрос для выборки всех строк из таблицы tasks
.query_map
выполняет запрос и преобразует каждую строку в кортеж (i32, String, bool)
с помощью замыкания.row.get(индекс)
извлекает значение столбца по его позиции (0 — id
, 1 — description
, 2 — done
).collect()
собирает результаты в Vec
.Нюанс: Если таблица пуста, функция вернёт пустой вектор. Убедитесь, что ваш код обработки вывода корректно обрабатывает этот случай.
Чтобы пометить задачу как выполненную, добавим функцию обновления:
fn complete_task(conn: &Connection, id: i32) -> Result<()> {
conn.execute(
"UPDATE tasks SET done = 1 WHERE id = ?1",
[id],
)?;
Ok(())
}
Здесь:
UPDATE tasks SET done = 1
устанавливает статус задачи в true
(1).WHERE id = ?1
указывает, какую задачу обновить, по её идентификатору.Подводный камень: Если id
не существует, запрос выполнится успешно, но не изменит ни одной строки. Вы можете проверить количество затронутых строк с помощью conn.changes()
, чтобы убедиться, что обновление прошло.
Для удаления задачи реализуем следующую функцию:
fn delete_task(conn: &Connection, id: i32) -> Result<()> {
conn.execute(
"DELETE FROM tasks WHERE id = ?1",
[id],
)?;
Ok(())
}
Этот код удаляет строку из таблицы tasks
по указанному id
. Как и в случае с обновлением, стоит проверить, была ли строка удалена, если это критично для логики приложения.
Теперь соберём все функции в полноценное консольное приложение. Оно будет поддерживать следующие команды:
add "описание задачи"
— добавляет новую задачу.list
— отображает все задачи.complete id
— помечает задачу как выполненную.delete id
— удаляет задачу.Вот полный код приложения:
use std::env;
use rusqlite::{Connection, Result};
fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
let conn = Connection::open("tasks.db")?;
create_table(&conn)?;
if args.len() < 2 {
println!("Usage: todo_list [add \"description\" | list | complete id | delete id]");
return Ok(());
}
match args[1].as_str() {
"add" => {
if args.len() < 3 {
println!("Please provide a description");
return Ok(());
}
add_task(&conn, &args[2])?;
println!("Task added");
}
"list" => {
let tasks = list_tasks(&conn)?;
for task in tasks {
println!("{}: {} [{}]", task.0, task.1, if task.2 { "done" } else { "not done" });
}
}
"complete" => {
if args.len() < 3 {
println!("Please provide a task id");
return Ok(());
}
let id: i32 = args[2].parse().expect("Invalid id");
complete_task(&conn, id)?;
println!("Task completed");
}
"delete" => {
if args.len() < 3 {
println!("Please provide a task id");
return Ok(());
}
let id: i32 = args[2].parse().expect("Invalid id");
delete_task(&conn, id)?;
println!("Task deleted");
}
_ => println!("Invalid command"),
}
Ok(())
}
fn create_table(conn: &Connection) -> Result<()> {
conn.execute(
"CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY,
description TEXT NOT NULL,
done BOOLEAN NOT NULL DEFAULT 0
)",
[],
)?;
Ok(())
}
fn add_task(conn: &Connection, description: &str) -> Result<()> {
conn.execute("INSERT INTO tasks (description) VALUES (?1)", [description])?;
Ok(())
}
fn list_tasks(conn: &Connection) -> Result<Vec<(i32, String, bool)>> {
let mut stmt = conn.prepare("SELECT id, description, done FROM tasks")?;
let tasks = stmt.query_map([], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
})?.collect();
tasks
}
fn complete_task(conn: &Connection, id: i32) -> Result<()> {
conn.execute("UPDATE tasks SET done = 1 WHERE id = ?1", [id])?;
Ok(())
}
fn delete_task(conn: &Connection, id: i32) -> Result<()> {
conn.execute("DELETE FROM tasks WHERE id = ?1", [id])?;
Ok(())
}
Как это работает:
Connection::open("tasks.db")
открывает файл базы данных tasks.db
в текущей директории. Если файла нет, он будет создан.env::args()
собирает аргументы командной строки.match
обрабатывает команды, вызывая соответствующие функции.id: описание [статус]
.Совет: Для улучшения приложения добавьте обработку ошибок ввода (например, если id
не число) с помощью match args[2].parse()
вместо expect
.
Реализуйте описанное выше консольное приложение для управления списком дел, используя базу данных SQLite. Убедитесь, что все CRUD-операции работают корректно. Проверьте приложение, добавив несколько задач, просмотрев их, завершив одну и удалив другую. Для дополнительного вызова:
edit id "новое описание"
для редактирования описания задачи.diesel
, вместо прямых SQL-запросов, и сравните удобство работы.id
не найдена).После выполнения сохраните код в файл src/main.rs
, соберите проект с помощью cargo build
и запустите его командой cargo run -- add "Купить молоко"
. Удачи!