Перейти к содержанию

Начало работы - Diesel CRUD в PostgreSQL

Posted on:13 июня 2023 г. at 08:37

Example Dynamic OG Image link

Diesel

Diesel - это безопасный, расширяемый ORM и построитель запросов для Rust.

Diesel является наиболее продуктивным способом взаимодействия с базами данных в Rust из-за его безопасных и компонуемых абстракций над запросами.

Для этого руководства мы рассмотрим несколько простых примеров для каждого из разделов CRUD, который означает «Create Read Update Delete». Каждый шаг в этом руководстве будет основан на предыдущем и должен следовать вперед.

Необходимые условия

Убедитесь, что PostgreSQL установлен и запущен.

PostgreSQL: получить

Diesel требует Rust 1.31 или выше. Если вы следуете вместе с этим руководством, убедитесь, что вы используете по крайней мере эту версию Rust, запустив rustup update stable.

Применение

Инициализация нового проекта

Первое, что нам нужно сделать, это создать наш проект.

# Создание нового проекта
cargo new --lib diesel_demo
cd diesel_demo
# Cargo.toml

[dependencies]
diesel = { version = "1.4.4", features = ["postgres"] }
dotenv = "0.15.0"

Установка интерфейса командной строки Diesel

Diesel предоставляет отдельный CLI-инструмент для управления проектом. Поскольку это автономный двоичный файл и не влияет непосредственно на код проекта, мы не добавляем его в файл Cargo.toml. Вместо этого мы просто устанавливаем его в нашу систему.

# Установите инструмент CLI.
$ cargo install diesel_cli

# Установите только diesel_cli PostgreSQL.
cargo install diesel_cli --no-default-features --features postgres

Создание базы данных

Нам нужно создать базу данных с именем пользователя и паролем.

-- psql

CREATE USER diesel_demo WITH PASSWORD 'diesel_demo';

CREATE DATABASE diesel_demo OWNER diesel_demo;

GRANT ALL PRIVILEGES ON DATABASE diesel_demo TO diesel_demo;

Настройка Diesel для проекта

Мы должны сказать Diesel, где найти нашу базу данных. Для этого необходимо задать переменную среды DATABASE_URL. На наших машинах для разработки, скорее всего, будет реализовано несколько проектов, и мы не хотим загрязнять окружающую среду. Вместо этого можно поместить URL-адрес в файл .env.

echo DATABASE_URL=postgres://diesel_demo:diesel_demo@localhost/diesel_demo > .env

Теперь Diesel CLI может настроить все для нас.

diesel setup

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

$ tree migrations
migrations
└── 00000000000000_diesel_initial_setup
    ├── down.sql
    └── up.sql

Первое, что нам понадобится, это таблица для хранения наших сообщений. Давайте создадим миграцию.

Diesel CLI создаст для нас два пустых файла в требуемой структуре. Вы увидите выходные сообщения, которые выглядят примерно так:

$ diesel migration generate create_posts
Creating migrations/20210626133237_create_posts/up.sql
Creating migrations/20210626133237_create_posts/down.sql

Миграции позволяют с течением времени развивать схему базы данных. Каждая миграция может быть применена (up.sql) или возвращена (down.sql). Применение и немедленный возврат миграции должны оставить схему базы данных без изменений.

Далее мы напишем SQL для миграции:

up.sql

CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  title VARCHAR NOT NULL,
  body TEXT NOT NULL,
  published BOOLEAN NOT NULL DEFAULT 'f'
);

down.sql

DROP TABLE posts;

Мы можем применить нашу новую миграцию:

$ diesel migration run
Running migration 2021-06-26-133237_create_posts

Рекомендуется убедиться, что файл down.sql верен. Вы можете быстро подтвердить, что ваш down.sql откатывает вашу миграцию правильно, повторив миграцию:

$ diesel migration redo
Rolling back migration 2021-06-26-133237_create_posts
Running migration 2021-06-26-133237_create_posts

Примечание по Raw SQL в миграциях:

Поскольку миграции записываются в raw SQL, они могут содержать определенные функции используемой системы базы данных. Например, приведенная выше инструкция CREATE TABLE использует тип PostgreSQL SERIAL. Если вместо этого необходимо использовать SQLite, вместо него необходимо использовать INTEGER.

Примечание по использованию миграции на лету:

Пакет diesel_migrations 1.4.0 - Docs.rs - https://docs.rs/crate/diesel_migrations/1.4.0 обеспечивает embed_migrations! , позволяя встраивать сценарии миграции в окончательный двоичный файл. После использования кода можно просто включить embedded_migrations::run(&db_conn) в начале основной функции для запуска миграции при каждом запуске приложения.

Пишем в rust

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

// src/lib.rs

pub mod schema;
pub mod models;

#[macro_use]
extern crate diesel;
extern crate dotenv;

use diesel::prelude::*;
use diesel::pg::PgConnection;
use dotenv::dotenv;
use std::env;

pub fn establish_connection() -> PgConnection {
    dotenv().ok();

    let database_url = env::var("DATABASE_URL")
        .expect("DATABASE_URL must be set");
    PgConnection::establish(&database_url)
        .expect(&format!("Error connecting to {}", database_url))
}

Далее нам нужно создать два модуля, которые мы только что объявили.

// src/models.rs

#[derive(Queryable)]
pub struct Post {
	pub id: i32,
	pub title: String,
	pub body: String,
	pub published: bool,
}

[derive (Queryable)] создаст весь код, необходимый для загрузки структуры Post из SQL запроса.

Обычно модуль схемы создается не вручную, а Diesel. Когда мы запустили установку diesel setup, был создан файл diesel.toml, который говорит Diesel поддерживать файл на src/schema.rs для нас. Файл должен выглядеть следующим образом:

// src/schema.rs

table! {
    posts (id) {
        id -> Integer,
        title -> Text,
        body -> Text,
        published -> Bool,
    }
}

Точные выходные данные могут несколько отличаться в зависимости от базы данных, но должны быть эквивалентными.

table! создает пакет кода на основе схемы базы данных для представления всех таблиц и столбцов. Мы посмотрим, как именно использовать это в следующем примере.

При каждом запуске или возврате миграции этот файл будет автоматически обновлен.

Примечание по полю упорядочивания

Использование #[derive(Queryable)] предполагает, что порядок полей в структуре публикаций соответствует столбцам в таблице публикаций, поэтому необходимо определить их в порядке, указанном в файле schema.rs.

Показать публикации

Давайте напишем код, чтобы показать нам наши публикации.

// src/bin/show_posts.rs

extern crate diesel_demo;
extern crate diesel;

use self::diesel_demo::*;
use self::models::*;
use self::diesel::prelude::*;

fn main() {
    use diesel_demo::schema::posts::dsl::*;

    let connection = establish_connection();
    let results = posts.filter(published.eq(true))
        .limit(5)
        .load::<Post>(&connection)
        .expect("Error loading posts");

    println!("Displaying {} posts", results.len());
    for post in results {
        println!("{}", post.title);
        println!("----------\n");
        println!("{}", post.body);
    }
}

При использовании diesel_demo::schema::posts::dsl::* строка импортирует набор псевдонимов, чтобы было вместо posts::table и опубликованных вместо posts::published. Это полезно, когда мы имеем дело только с одной таблицей, но это не всегда то, чего мы хотим.

Мы можем запустить наш сценарий cargo run--bin show_posts. К сожалению, результаты не будут ужасно интересными, так как у нас на самом деле нет никаких записей в базе данных. Тем не менее, мы написали приличное количество кода, так что давайте продолжим.

Полный код для демонстрации на данном этапе можно найти здесь v1.4.4· diesel-rs/diesel - https://github.com/diesel-rs/diesel/tree/v1.4.4/examples/postgres/getting_started_step_1/.

Создать публикацию

Затем напишем код, чтобы создать новую публикацию или поста. Нам нужна структура для вставки новой записи.

// src/models.rs

use super::schema::posts;

#[derive(Insertable)]
#[table_name="posts"]
pub struct NewPost<'a> {
    pub title: &'a str,
    pub body: &'a str,
}

Теперь давайте добавим функцию для сохранения новой записи.

// src/lib.rs

use self::models::{Post, NewPost};

pub fn create_post<'a>(conn: &PgConnection, title: &'a str, body: &'a str) -> Post {
    use schema::posts;

    let new_post = NewPost {
        title: title,
        body: body,
    };

    diesel::insert_into(posts::table)
        .values(&new_post)
        .get_result(conn)
        .expect("Error saving new post")
}

Когда мы вызываем .get_result в инструкции insert или update, она автоматически добавляет RETURN * в конец запроса и позволяет загрузить его в любую структуру, которая реализует Queryable для нужных типов.

Diesel может вставить несколько записей в один запрос. Просто передайте Vec или срез для вставки, а затем вызовите get_results вместо get_result. Если вы не хотите ничего делать с только что вставленной строкой, вызовите execute. Компилятор не будет жаловаться на вас, таким образом. :)

Теперь, когда все настроено, мы можем создать небольшой сценарий, чтобы записать новый пост.

// src/bin/write_post.rs

extern crate diesel_demo;
extern crate diesel;

use self::diesel_demo::*;
use std::io::{stdin, Read};

fn main() {
    let connection = establish_connection();

    println!("What would you like your title to be?");
    let mut title = String::new();
    stdin().read_line(&mut title).unwrap();
    let title = &title[..(title.len() - 1)]; // Drop the newline character
    println!("\nOk! Let's write {} (Press {} when finished)\n", title, EOF);
    let mut body = String::new();
    stdin().read_to_string(&mut body).unwrap();

    let post = create_post(&connection, title, &body);
    println!("\nSaved draft {} with id {}", title, post.id);
}

#[cfg(not(windows))]
const EOF: &'static str = "CTRL+D";

#[cfg(windows)]
const EOF: &'static str = "CTRL+Z";

Мы можем запустить наш новый сценарий cargo run --bin write_post. Вперед и написать пост для блога. Получите креатив! Вот мой:

$ cargo run --bin write_post
   Compiling diesel_demo v0.1.0 (file:///Users/cloudolife/diesel_demo)
     Running `target/debug/write_post`

What would you like your title to be?
Diesel demo

Ok! Let's write Diesel demo (Press CTRL+D when finished)

You know, a CLI application probably isn't the best interface for a blog demo.
But really I just wanted a semi-simple example, where I could focus on Diesel.
I didn't want to get bogged down in some web framework here.
Plus I don't really like the Rust web frameworks out there. We might make a
new one, soon.

Saved draft Diesel demo with id 1

К сожалению, запуск show_posts все еще не отобразит наш новый пост, потому что мы сохранили его как черновик. Если мы вернемся к коду в show_posts, мы добавили .filter (published.eq (true)) и опубликовали значение по умолчанию false в нашей миграции. Мы должны опубликовать его! Но для этого нам нужно будет посмотреть, как обновить существующую запись. Сначала давайте сделаем. Код для этой демонстрации на данном этапе можно найти здесь.

Публикация поста

Теперь, когда мы создаем и читаем, обновление на самом деле относительно просто. Давайте перейдем прямо к сценарию:

// src/bin/publish_post.rs

extern crate diesel_demo;
extern crate diesel;

use self::diesel::prelude::*;
use self::diesel_demo::*;
use self::models::Post;
use std::env::args;

fn main() {
    use diesel_demo::schema::posts::dsl::{posts, published};

    let id = args().nth(1).expect("publish_post requires a post id")
        .parse::<i32>().expect("Invalid ID");
    let connection = establish_connection();

    let post = diesel::update(posts.find(id))
        .set(published.eq(true))
        .get_result::<Post>(&connection)
        .expect(&format!("Unable to find post {}", id));
    println!("Published post {}", post.title);
}

Вот и все! Попробуем cargo run --bin publish_post 1.

$ cargo run --bin publish_post 1
 Compiling diesel_demo v0.1.0 (file:///Users/cloudolife/diesel_demo)
   Running `target/debug/publish_post 1`
Published post Diesel demo
And now, finally, we can see our post with cargo run --bin show_posts.

     Running `target/debug/show_posts`
Displaying 1 posts
Diesel demo
----------

You know, a CLI application probably isn't the best interface for a blog demo.
But really I just wanted a semi-simple example, where I could focus on Diesel.
I didn't want to get bogged down in some web framework here.
Plus I don't really like the Rust web frameworks out there. We might make a
new one, soon.

Мы все еще покрыли только три из четырех задач CRUD. Давайте покажем, как удалять записи. Иногда мы пишем то, что на самом деле ненавидим, и у нас нет времени искать удостоверение личности. Поэтому давайте удалим, основываясь на названии, или даже просто некоторые слова в названии.

// src/bin/delete_post.rs

extern crate diesel_demo;
extern crate diesel;

use self::diesel::prelude::*;
use self::diesel_demo::*;
use std::env::args;

fn main() {
    use diesel_demo::schema::posts::dsl::*;

    let target = args().nth(1).expect("Expected a target to match against");
    let pattern = format!("%{}%", target);

    let connection = establish_connection();
    let num_deleted = diesel::delete(posts.filter(title.like(pattern)))
        .execute(&connection)
        .expect("Error deleting posts");

    println!("Deleted {} posts", num_deleted);
}

Мы можем запустить сценарий cargo run --bin delete_post demo (по крайней мере, с заголовок я выбрал). Ваш результат должен выглядеть примерно так:

$ cargo run --bin delete_post
   Compiling diesel_demo v0.1.0 (file:///Users/cloudolife/diesel_demo)
     Running `target/debug/delete_post demo`
Deleted 1 posts

Когда мы пытаемся запустить cargo run --bin show_posts снова, мы видим, что пост был фактически удален. Это едва царапает поверхность того, что вы можете сделать с Diesel, но, надеюсь, это учебное пособие дало вам хороший фундамент, чтобы строить. Для получения дополнительной информации рекомендуется изучить документы API. Окончательный код для этого учебного пособия можно найти здесь v1.4.4· diesel-rs/diesel - https://github.com/diesel-rs/diesel/tree/v1.4.4/examples/postgres/getting_started_step_3/.

FAQs

не удается найти таблицу макросов в этой области, невозможно найти производный макрос Queryable в этой области

$ cargo build
...
error: cannot find macro `table` in this scope
 --> src/schema.rs:1:1
  |
1 | table! {
  | ^^^^^
  |
  = note: consider importing this macro:
          diesel::table

error: cannot find derive macro `Queryable` in this scope
 --> src/models.rs:2:10
  |
2 | #[derive(Queryable)]
  |          ^^^^^^^^^

error: aborting due to 2 previous errors

error: could not compile `diesel_demo`
...
// src/lib.rs

// Включить использование diesel макроса
#[macro_use]
extern crate diesel;