Как стать автором
Обновить

Создаём REST-сервис на PostgreSQL и Rust. Часть 1: прототип

Время на прочтение 6 мин
Количество просмотров 37K
Всем привет.

Какое-то время назад я видел в небезызвестном блоге пост о том, как реализовать на Go простую программу, работающую с БД, а затем сделать на её базе целый REST-сервис. Я решил проверить, насколько сложно сделать аналогичную программу на Rust и поделиться результатами.



Мы начнём с работы с БД и создадим обычное консольное приложение, а затем добавим, так сказать, REST-фронтенд.

Несколько вступительных замечаний.

Для нетерпеливых — вот законченный проект на GitHub. Он включает в себя и реализацию REST-сервиса. Всех остальных же приглашаю читать дальше.

В целом, я постараюсь подробно проиллюстрировать процесс разработки со всеми ошибками, касающимися Rust, их причинами и способами устранения. Я думаю, знание типичных проблем и способов их решения сильно помогает новичкам в языке. Не бойтесь компилятора, он — ваш друг.

Вам понадобится установленный Rust (как установить). Должна работать любая версия после 1.0 — как stable, так и nightly. Я пробовал несколько в интервале 1.1-1.3.

Сам код здесь прототипного качества — я сейчас не пытаюсь сделать очень надёжную или читабельную программу. Разобравшись в нём, можно будет задуматься о правильности и стиле позже. Однако, и написана эта версия была весьма быстро.

Теперь к делу.

Как и любой проект на Rust, не требующий особых хитростей со сборкой, наша программа будет использовать Cargo. Создадим новый проект:

$ cargo new --bin rust-phonebook
$ cd rust-phonebook

Cargo заботливо создаёт в директории репозиторий Git.

Как это выглядит
$ git status

On branch master

Initial commit

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	.gitignore
	Cargo.toml
	src/

nothing added to commit but untracked files present (use "git add" to track)


И мы можем сразу собрать и запустить нашу программу-заглушку:

$ cargo run

   Compiling rust-phonebook v0.1.0 (file:///home/mkpankov/rust-phonebook)
     Running `target/debug/rust-phonebook`
Hello, world!

После чего закоммитим наши изменения в репозиторий и перейдём к сути нашей программы.



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

Сначала я приведу весь код целиком, а затем объясню каждую его часть. Ниже — содержимое src/main.rs.

Код
extern crate postgres;

use postgres::{Connection, SslMode};

struct Person {
    id: i32,
    name: String,
    data: Option<Vec<u8>>
}

fn main() {
    let conn =
        Connection::connect(
            "postgres://postgres:postgres@localhost",
            &SslMode::None)
        .unwrap();

    conn.execute(
        "CREATE TABLE person (
           id              SERIAL PRIMARY KEY,
           name            VARCHAR NOT NULL,
           data            BYTEA
         )",
        &[])
        .unwrap();

    let me = Person {
        id: 0,
        name: "Михаил".to_string(),
        data: None
    };

    conn.execute(
        "INSERT INTO person (name, data) VALUES ($1, $2)",
        &[&me.name, &me.data])
        .unwrap();

    let stmt = conn.prepare("SELECT id, name, data FROM person").unwrap();

    for row in stmt.query(&[]).unwrap() {
        let person = Person {
            id: row.get(0),
            name: row.get(1),
            data: row.get(2)
        };
        println!("Нашли человека: {}", person.name);
    }
}


Давайте разберём все по порядку.

fn main() {
    let conn =
        Connection::connect(
            "postgres://postgres:postgres@localhost",
            &SslMode::None)
        .unwrap();

Первая строка в нашем новом main — подключение к БД. Тут стоит сразу рассказать подробнее.

Мы предполагаем, что локально запущен сервер PostgreSQL на порту по умолчанию, а имя пользователя и пароль — «postgres». Для этого нам, конечно, нужно установить PostgreSQL. Можно посмотреть, например, это руководство. Укажите ваше имя пользователя, имеющего доступ к базе, и его пароль вместо «postgres:postgres».

Помимо этого, не забудьте инициализировать базу данных.

Сам вышеупомянутый Connection — тип из контейнера postgres (документация). Поэтому мы запрашиваем его связывание вверху файла

extern crate postgres;

и вводим в область видимости Connection и SslMode

use postgres::{Connection, SslMode};

Если попробовать собрать программу прямо сейчас, вы получите другую ошибку:

$ cargo build
   Compiling rust-phonebook v0.1.0 (file:///home/mkpankov/rust-phonebook.finished)
src/main.rs:1:1: 1:23 error: can't find crate for `postgres`
src/main.rs:1 extern crate postgres;
              ^~~~~~~~~~~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `rust-phonebook`.

To learn more, run the command again with --verbose.

Это означает, что компилятор не нашёл подходящий контейнер. Это потому, что мы не указали его в зависимостях нашего проекта. Давайте сделаем это в Cargo.toml (подробнее):

[dependencies]

postgres = "0.9"

Теперь всё должно собираться. Но если вы не запустили сервер, то получите такую ошибку:

 
$ cargo run
     Running `target/debug/rust-phonebook`
thread '<main>' panicked at 'called `Result::unwrap()` on an `Err` value: IoError(Error { repr: Os { code: 111, message: "Connection refused" } })', ../src/libcore/result.rs:732

Это непосредственный результат нашего .unwrap() — он вызывает панику текущего потока, если Result был не Ok(_) — т.е. произошла ошибка соединения.

Кстати, backtrace для неё можно увидеть, если запустить программу с выставленным RUST_BACKTRACE=1 в окружении (работает только в отладочной версии программы!).

Backtrace
$ RUST_BACKTRACE=1 cargo run

     Running `target/debug/rust-phonebook`
thread '<main>' panicked at 'called `Result::unwrap()` on an `Err` value: IoError(Error { repr: Os { code: 111, message: "Connection refused" } })', ../src/libcore/result.rs:732
stack backtrace:
   1:     0x56007b30a95e - sys::backtrace::write::haf6e4e635ac76143Ivs
   2:     0x56007b30df06 - panicking::on_panic::ha085a58a08f78856lzx
   3:     0x56007b3049ae - rt::unwind::begin_unwind_inner::hc90ee27246f12475C0w
   4:     0x56007b304ee6 - rt::unwind::begin_unwind_fmt::ha4be06289e0df3dbIZw
   5:     0x56007b30d8d6 - rust_begin_unwind
   6:     0x56007b3390c4 - panicking::panic_fmt::he7875691f9cbe589SgC
   7:     0x56007b25e58d - result::Result<T, E>::unwrap::h10659124002062427088
                        at ../src/libcore/macros.rs:28
   8:     0x56007b25dcfd - main::h2f2e9aa4b99bad67saa
                        at src/main.rs:13
   9:     0x56007b30d82d - __rust_try
  10:     0x56007b30fbca - rt::lang_start::hefba4015e797c325hux
  11:     0x56007b27d1ab - main
  12:     0x7fb3f21076ff - __libc_start_main
  13:     0x56007b25db48 - _start
  14:                0x0 - <unknown>


Фух, всего одна строчка, а столько способов накосячить! Надеюсь, вы не сильно напуганы и готовы продолжать.

Положительным моментом здесь является то, что мы явно говорим, что хотим уронить программу при ошибке соединения. Когда мы захотим сделать из нашей игрушки нормальный продукт, простой текстовый поиск по .unwrap() покажет, с чего стоит начать. Дальше я не буду останавливаться на этом моменте.

Создаём таблицу:

    conn.execute(
        "CREATE TABLE person (
           id              SERIAL PRIMARY KEY,
           name            VARCHAR NOT NULL,
           data            BYTEA
         )",
        &[])
        .unwrap();

Странное &[] в конце — это пустой срез. У данного запроса нет параметров, поэтому мы не передаём их.

Почему срез, а не массив? Хороший стиль в Rust — не принимать владение, если объекты нужны только для чтения. Иначе нам пришлось бы клонировать значение для передачи в функцию, т.е. она «поглотила» бы его. Подробнее о владении читайте тут.

Далее мы создаём структуру, представляющую собой нашу запись, которую мы будем добавлять в таблицу:

    let me = Person {
        id: 0,
        name: "Михаил".to_string(),
        data: None
    };

Принципиально, сейчас смысла складывать эти данные в структуру нет, но дальше это нам поможет. Кстати, вот её объявление:

struct Person {
    id: i32,
    name: String,
    data: Option<Vec<u8>>
}

Теперь выполним собственно вставку:

    conn.execute(
        "INSERT INTO person (name, data) VALUES ($1, $2)",
        &[&me.name, &me.data])
        .unwrap();

Здесь уже у нашего запроса есть параметры. Они подставляются с помощью строковой интерполяции в нумерованные поля $1, $2 и т.д. И теперь наш срез параметров не пуст — он содержит ссылки на соответствующие поля структуры.

Далее мы подготавливаем запрос к базе, чтобы прочитать то, что записали:

    let stmt = conn.prepare("SELECT id, name, data FROM person").unwrap();

Думаю, ничего интересного. Это просто создание объекта-запроса. Повторяющиеся запросы имеет смысл не пересоздавать, а хранить, для увеличения производительности. Мы также могли бы сразу выполнить запрос без создания «приготовленного объекта».

В конце мы выполняем сам запрос. Пройдёмся по каждой строчке:

    for row in stmt.query(&[]).unwrap() {

Здесь мы обходим массив результатов запроса. Как всегда, запрос мог бы завершиться ошибкой. Список параметров снова пуст — &[].

Теперь снова собираем структуру из результатов запроса.

        let person = Person {
            id: row.get(0),
            name: row.get(1),
            data: row.get(2)
        };

Здесь мы просто берём поля по номерам, но вообще библиотека позволяет использовать и имена полей таблицы.

Наконец, печатаем сообщение с результатом:

        println!("Нашли человека: {}", person.name);
    }
}

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

В следующей части мы добавим конфигурацию сервера в INI-файле. Оставайтесь с нами!
Теги:
Хабы:
+29
Комментарии 28
Комментарии Комментарии 28

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн