Создаём REST-сервис на Rust. Часть 3: обновляем базу из консоли
Всем привет!
В предыдущей части мы разобрали конфигурационный файл базы данных, чтобы считать из него параметры соединения.
Теперь давайте реализуем непосредственно операции обновления БД: создание, обновление, удаление наших записей и соответствующий им интерфейс командной строки.
Для начала, давайте разберём аргументы программы. Её интерфейс будет выглядеть так:
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-интерфейса. Веб-частью мы займёмся в следующий раз.