Системный программист
3,2
рейтинг
28 сентября 2015 в 12:27

Разработка → Создаём REST-сервис на Rust. Часть 3: обновляем базу из консоли tutorial

Rust*
В предыдущей части мы разобрали конфигурационный файл базы данных, чтобы считать из него параметры соединения.

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

Для начала, давайте разберём аргументы программы. Её интерфейс будет выглядеть так:

const HELP: &'static str = "Usage: phonebook COMMAND [ARG]...
Commands:
    add NAME PHONE - create new record;
    del ID1 ID2... - delete record;
    edit ID        - edit record;
    show           - display all records;
    show STRING    - display records which contain a given substring in the name;
    help           - display this help.";

Здесь уже есть пара интересных моментов. const объявляет постоянную, причём такую, что она просто встраивается в место использования. Таким образом, у неё нет своего адреса в памяти — похоже на #define в C. Тип постоянной надо указывать всегда — и в данном случае он может выглядеть немного пугающе. &’static str? Что это?

Если мне не изменяет память, явно указанных времён жизни мы ещё не видели. Так вот, это — ссылка, &str, и её можно по-другому записать как &’foo str. Обычно нам не приходится явно указывать время жизни, т.к. компилятор может сам вывести его — т.е. ‘foo просто опускается.

Отмечу также, что ‘foo могло бы быть ‘bar или чем угодно ещё — это просто имя переменной. В нашем случае, можно думать так: ссылка HELP: &str имеет время жизни, называемое ‘foo, и оно равно ‘static.

Теперь о ‘static. Это время жизни, равное времени жизни программы. Наша строка непосредственно встроена в образ программы, и ей не требуется какая-либо инициализация или явное уничтожение. Поэтому она доступна всегда, пока программа исполняется. Подробнее о ‘static можно прочитать здесь.

Таким образом, мы объявили строковую постоянную, которая всегда доступна.

А вот код разбора аргументов — как всегда, сначала целиком. Затем мы рассмотрим его подробнее.

Код разбора командной строки
    let args: Vec<String> = std::env::args().collect();
    match args.get(1) {
        Some(text) => {
            match text.as_ref() {
                "add" => {
                    if args.len() != 4 {
                        panic!("Usage: phonebook add NAME PHONE");
                    }
                    let r = db::insert(db, &args[2], &args[3])
                        .unwrap();
                    println!("{} rows affected", r);
                        },
                "del" => {
                    if args.len() < 3 {
                        panic!("Usage: phonebook del ID...");
                    }
                    let ids: Vec<i32> = args[2..].iter()
                        .map(|s| s.parse().unwrap())
                        .collect();

                    db::remove(db, &ids)
                        .unwrap();
                },
                "edit" => {
                    if args.len() != 5 {
                        panic!("Usage: phonebook edit ID NAME PHONE");
                    }
                    let id = args[2].parse().unwrap();
                    db::update(db, id, &args[3], &args[4])
                        .unwrap();
                },
                "show" => {
                    if args.len() > 3 {
                        panic!("Usage: phonebook show [SUBSTRING]");
                    }
                    let s;
                    if args.len() == 3 {
                            s = args.get(2);
                    } else {
                        s = None;
                    }
                    let r = db::show(db, s.as_ref().map(|s| &s[..])).unwrap();
                    db::format(&r);
                },
                "help" => {
                    println!("{}", HELP);
                },
                command @ _  => panic!(
                    format!("Invalid command: {}", command))
            }
        }
        None => panic!("No command supplied"),
    }


Посмотрим на первую строку:

    let args: Vec<_> = std::env::args().collect();

std::env::args() просто возвращает итератор по аргументам командной строки. Почему это итератор, а не какой-нибудь статический массив? Потому что нам могут и не понадобиться все аргументы, а потенциально их может быть много. Поэтому используется итератор — он «ленив». Это в духе Rust — вы не платите за то, что вам не нужно.

Так вот, здесь у нас заведомо мало аргументов и нам будет проще иметь всё-таки нормальный вектор, из которого аргументы можно брать по индексам. Мы делаем .collect(), чтобы обойти все элементы и собрать их в определённую коллекцию.

Какую именно коллекцию? Вот тут есть тонкий момент. На самом деле, .collect() вызывает метод from_iter() той коллекции, в которую кладутся элементы. Получается, нам нужно знать её тип. Именно поэтому мы не можем опустить тип args и написать так:

    let args = std::env::args().collect();

Вот что на это скажет компилятор:

main.rs:61:9: 61:13 error: unable to infer enough type information about `_`; type annotations or generic parameter binding required [E0282]
main.rs:61     let args = std::env::args().collect();
                   ^~~~
main.rs:61:9: 61:13 help: run `rustc --explain E0282` to see a detailed explanation

Однако заметьте, что вывод типов делает своё дело: нам достаточно указать в качестве типа Vec<_>: какой тип лежит в векторе, компилятор и так знает. Нужно только уточнить, какую коллекцию мы хотим.

Ну и зачем все эти сложности? Затем, что мы можем, например, собрать аргументы в связный список (или какую-то другую коллекцию), если захотим:

    let args: std::collections::LinkedList<_> = std::env::args().collect();

Список коллекций, реализующих from_iter, есть на странице документации типажа.

Далее мы видим:

    match args.get(1) {

.get() возвращает Ok(element), если элемент вектора существует, и None в противном случае. Мы пользуемся этим, чтобы обнаружить ситуацию, когда пользователь не указал команду:

        }
        None => panic!("No command supplied"),
    }

Если команда не совпадает ни с одной из предопределённых, мы выводим ошибку:

                command @ _  => panic!(
                    format!("Invalid command: {}", command))

Мы хотим попасть в эту ветвь при любом значении text — поэтому в качестве значения данной ветви используется _, «любое значение». Однако, мы хотим вывести эту самую неправильную команду, поэтому мы связываем выражение match с именем command с помощью конструкции command @ _. Подробнее об этом синтаксисе смотрите здесь и здесь.

Дальше разбор выглядит так:

        Some(text) => {
            match text.as_ref() {
                "add" => {
                    // handle add
                },

Если у нас есть команда, мы попадём в ветвь Some(text). Далее мы пользуемся match ещё раз, чтобы сопоставить название команды — как видите, match довольно универсален.

Команды разбираются довольно однотипно, поэтому давайте рассмотрим самую интересную: delete. Она принимает список идентификаторов записей, которые должны быть удалены.

                "del" => {
                    if args.len() < 3 {
                        panic!("Usage: phonebook del ID...");
                    }
                    let ids: Vec<i32> = args[2..].iter()
                        .map(|s| s.parse().unwrap())
                        .collect();

                    db::remove(db, &ids)
                        .unwrap();
                },

Сначала нам нужны идентификаторы: мы получаем их из аргументов командной строки следующим образом:

                    let ids: Vec<i32> = args[2..].iter()
                        .map(|s| s.parse().unwrap())
                        .collect();

С let foo: Vec<_> =… .collect() мы уже знакомы. Осталось разобраться, что происходит внутри этой строчки.

args[2..] получает срез вектора — начиная с третьего элемента до конца вектора. Похоже на срезы в Python.

.iter() получает итератор по этому срезу, к которому мы применяем анонимную функцию с помощью .map():

                        .map(|s| s.parse().unwrap())

Наша анонимная функция принимает единственный аргумент — s — и разбирает его как целое число. Откуда она знает, что это должно быть целое? Отсюда:

                    let ids: Vec<i32> = 

(Хе-хе, на самом деле, даже не отсюда, а из сигнатуры функции db::remove — она принимает срез &[i32]. Вывод типов использует эту информацию, чтобы понять, что FromStr::from_str надо вызывать у i32. Поэтому мы могли быть и здесь использовать Vec<_> — но в целях документирования кода, мы указали тип явно. Про саму db::remove — ниже.)

Вообще, применение адаптеров итераторов вроде .map() — это распространённый шаблон в коде на Rust. Он позволяет получить контролируемую ленивость исполнения там, где она чаще всего нужна — при потоковом чтении каких-то данных.

Отлично, мы справились со всей подготовительной работой. Осталось обновить саму базу. insert выглядит совсем скучно. Давайте посмотрим на remove.

Кстати, а почему она записана как db::remove? Потому, что она находится в отдельном модуле. На уровне файлов, это значит, что она в отдельном исходнике: src/db.rs. Как этот модуль включается в наш главный файл? Вот так:

mod db;

Просто! Данная инструкция эквивалента вставке всего исходного кода модуля в то место, где она написана. (Но на самом деле этого не происходит, это же не сишный препроцессор. Тут компилируется весь контейнер сразу, поэтому компилятор может считать модули в память и устанавливать связи на уровне промежуточного представления, а не тупо копировать исходный код в виде текста.) Стоит отметить, что компилятор будет искать модуль в файлах src/db.rs и src/db/mod.rs — это позволяет аккуратно организовать иерархию модулей.

Теперь код нашей функции:

pub fn remove(db: Connection, ids: &[i32]) -> ::postgres::Result<u64> {
    let stmt = db.prepare("DELETE FROM phonebook WHERE id=$1").unwrap();
    for id in ids {
        try!(stmt.execute(&[id]));
    }
    Ok(0)
}

Так-так, здесь мы почти всё знаем. По порядку.

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

main.rs:81:21: 81:31 error: function `remove` is private
main.rs:81                     db::remove(db, &ids)
                               ^~~~~~~~~~

Тип возвращаемого значения выглядит странновато. ::postgres::Result?

Два двоеточия означают, что модуль postgres нужно искать от корня нашего контейнера, и не от текущего модуля. Этот модуль автоматически объявляется в main.rs, когда мы делаем extern crate postgres. Но он не становится виден в db.rs автоматически! Поэтому мы лезем в корень пространства имён с помощью ::postgres. Ещё мы могли бы повторно запросить связывание контейнера postgres в db.rs, но это не считается хорошей практикой — лучше, если все запросы на связывание находятся в одном месте, а остальные модули пользуются тем, что доступно в главном.

Хорошо, разобрались немного с модулями. Подробнее смотрите здесь.

Далее мы видим невиданный доселе макрос: try!.

Он, как подсказывает его название, пытается выполнить некую операцию. Если она завершается успехом, значением try!() будет значение, вложенное в Ok(_). Если нет, он выполняет нечто похожее на return Err(error). Это альтернатива нашим постоянным .unwrap() — теперь программа не завершится паникой в случае ошибки, а вернёт ошибку наверх для обработки вызывающей функцией.

Этим макросом можно пользоваться в функциях, которые сами возвращают Result — в противном случае макрос не сможет вернуть Err, т.к. тип возвращаемого значения и тип значения в return не совпадут.

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

Вот, например, как происходит работа с транзакциями:

{
    let tx: ::postgres::Transaction = db.transaction().unwrap();
    tx.execute(
            "UPDATE phonebook SET name = $1, phone = $2 WHERE id = $3",
            &[&name, &phone, &id]).unwrap();
    tx.set_commit();
}

Как видите, это типичное применение RAII. Мы просто не передаём никуда tx, и оно уничтожается по выходу из блока. Реализация его деструктора сохраняет или откатывает транзакцию в зависимости от флага успеха. Если бы мы не сделали tx.set_commit(), деструктор tx откатил бы её.

А вот как можно отформатировать строку без печати на экран:

    Some(s) => format!("WHERE name LIKE '%{}%'", s),

Когда мы создаём вектор, можно сразу указать, под сколько элементов он должен выделить память:

    let mut results = Vec::with_capacity(size);

И напоследок, ещё один пример кода в функциональном стиле:

    let max = rs.iter().fold(
        0,
        |acc, ref item|
        if item.name.len() > acc { item.name.len() } else { acc });

Этот код можно было бы записать проще, если бы мы сравнивали типы, для которых реализован типаж Ord:

    let max = rs.iter().max();

Либо, мы можем реализовать этот типаж для Record. Он требует реализации PartialOrd и Eq, а Eq, в свою очередь — PartialEq. Поэтому на самом деле придётся реализовать 4 типажа. К счастью, реализация тривиальна.

Реализация типажей
use std::cmp::Ordering;

impl Ord for Record {
    fn cmp(&self, other: &Self) -> Ordering {
            self.name.len().cmp(&other.name.len())
    }
}

impl PartialOrd for Record {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
            Some(self.name.len().cmp(&other.name.len()))
    }
}

impl Eq for Record { }

impl PartialEq for Record {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
                && self.name == other.name
                && self.phone == other.phone
    }
}

pub fn format(rs: &[Record]) {
    let max = rs.iter().max().unwrap();
    for v in rs {
        println!("{:3}   {:.*}   {}", v.id, max.name.len(), v.name, v.phone);
    }
}


Стоит отметить, что осмысленность такой реализации под вопросом — всё же вряд ли стоит сравнивать записи БД по длине одного из полей.

Кстати, типаж Eq — это один из примеров типажей-маркеров: он не требует реализации никаких методов, а просто говорит компилятору, что какой-то тип обладает определённым свойством. Другие примеры таких типажей — это Send и Sync, про которые мы ещё поговорим.

На сегодня всё — пост и так оказался самым длинным из серии.

Теперь наше приложение реально работает, но у него пока нет REST-интерфейса. Веб-частью мы займёмся в следующий раз.
Михаил Панков @mkpankov
карма
51,2
рейтинг 3,2
Системный программист
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (8)

  • 0
    command @ _ =>
    вполне можно заменить на command =>
  • 0
        let max = rs.iter().fold(
            0,
            |acc, ref item|
            if item.name.len() > acc { item.name.len() } else { acc });
    

    вполне заменяется на
        let max = rs.iter().map(|item| item.name.len()).max();
    

    И ref у вас лишний — item и так будет, скорее всего, ссылкой потому что используется iter(), но даже если и нет (если iter() на ResultSet'е возвращает значения, а не ссылки), то ref бессмысленнен — item будет передан в замыкание по значению в любом случае.

    И ещё, вместо разбора аргументов программы вручную я бы посоветовал воспользоваться какой-нибудь библиотекой, вроде docopt. Бонус docopt — читабельный хелп к программе.
    • +1
      >> docopt

      https://github.com/kbknapp/clap-rs же :)

      Он даже был пакетом недели недавно — http://this-week-in-rust.org/blog/2015/09/14/this-week-in-rust-96.
      • 0
        Ну лично мне docopt импонирует больше, хотя, насколько я вижу, clap-rs всё же будет пофункциональнее.
      • 0
        Да, где-то видел что это «лучшая библиотека разбора аргументов вообще, с которой приходилось работать» (по мнению одного из пользователей; мой вольный перевод).
      • +1
        Мне docopt больше нравится тем, что с его помощью можно десериализовать аргументы в готовую структуру с нужными типами. Как это сделать с помощью clap без головной боли я так и не понял. Ну лениво мне самому аргрументы в инты парсить, гораздо проще сделать так:

        struct Args {
          arg_count: usize,
        }
        
        let args: Args = Docopt::new(USAGE)
                                    .and_then(|d| d.decode())
                                    .unwrap_or_else(|e| e.exit());
        

    • 0
      Классно, спасибо.

      На самом деле, как вы можете догадаться, мелочи вроде ref присутствуют в коде потому, что когда-то я их добавил в попытках заставить программу компилироваться, а затем не удалил. Код в целом весьма старый на самом деле — июнь.

      docopt не стал пользоваться потому что показалось, что это достаточно просто сделать самому (в отличие от разбора INI). Заодно показал, как пользоваться match.
      • 0
        docopt не стал пользоваться потому что показалось, что это достаточно просто сделать самому (в отличие от разбора INI). Заодно показал, как пользоваться match.

        Ну если для обучения, то смысл это имеет, хотя показать различные библиотеки и посоветовать не переизобретать велосипеды тоже хорошо :)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.