Сега проучихме няколко различни библиотеки за някои производствени задачи в Rust. Преди няколко седмици използвахме Diesel, за да създадем ORM за някои типове бази данни. И тогава миналата седмица използвахме Rocket, за да направим основен уеб сървър, който да отговаря на основни заявки. Тази седмица ще обединим тези две идеи! Ще използваме някои по-разширени функции от Rocket, за да направим някои CRUD крайни точки за нашия тип база данни. Разгледайте кода на Github тук!

Ако никога не сте писали Rust, трябва да започнете с основите! Разгледайте нашата Rust серия за начинаещи!

Състояние на базата данни и екземпляри

Първата ни задача е да се свържем с базата данни от нашите манипулиращи функции. Има някои „директни интеграции“, които можете да проверите между Rocket, Diesel и други библиотеки. Те могат да осигурят хитри начини за добавяне на аргумент за връзка към всеки манипулатор.

Но засега ще запазим нещата прости. Ще генерираме повторно PgConnection във всяка крайна точка. Ние ще поддържаме низ за свързване със „състояние“, за да гарантираме, че всички те използват една и съща база данни.

Нашият сървър Rocket може да „управлява“ различни елементи на състоянието. Да предположим, че имаме функция, която ни дава низ от нашата база данни. Можем да го предадем на нашия сървър по време на инициализация.

fn local_conn_string() -> String {...}
fn main() {
  rocket::ignite()
    .mount("/", routes![...])
    .manage(local_conn_string())
    .launch();
}

Сега можем да осъществим достъп до това String от всяка от нашите крайни точки, като дадем вход от типа State<String>. Това ни позволява да създадем нашата връзка:

#[get(...)]
fn fetch_all_users(database_url: State<String>) -> ... {
  let connection = pgConnection.establish(&database_url)
    .expect("Error connecting to database!");
  ...
}

Забележка: Не можем да използваме самия PgConnection, тъй като типовете със състояние трябва да са нишково безопасни.

Така че всяка друга наша крайна точка вече може да има достъп до същата база данни. Преди да започнем да пишем тези, първо се нуждаем от няколко неща. Нека си припомним, че за нашия Diesel ORM направихме тип User и тип UserEntity. Първият е за вмъкване/създаване, а вторият е за заявки. Трябва да добавим някои екземпляри към тези типове, така че да са съвместими с нашите крайни точки. Искаме да имаме JSON екземпляри (Serialize, Deserialize), както и FromForm за нашия User тип:

#[derive(Insertable, Deserialize, Serialize, FromForm)]
#[table_name="users"]
pub struct User {
  ...
}
#[derive(Queryable, Serialize)]
pub struct UserEntity {
  ...
}

Сега нека да видим как получаваме тези типове от нашите крайни точки!

Извличане на потребители

Ще започнем с проста крайна точка, за да извлечем всички различни потребители в нашата база данни. Това няма да изисква никакви входни данни, освен нашия URL адрес на база данни със състояние. Той ще върне вектор от UserEntity обекти, обвити в Json.

#[get("/users/all")]
fn fetch_all_users(database_url: State<String>)
  -> Json<Vec<UserEntity>> {
    ...
}

Сега всичко, което трябва да направим, е да се свържем с нашата база данни и да изпълним функцията за заявка. Можем да направим нашите потребители векторни в Json обект чрез обвиване с Json(). Екземплярът Serialize ни позволява да удовлетворим характеристиката Responder за върнатата стойност.

#[get("/users/all")]
fn fetch_all_users(database_url: State<String>)
  -> Json<Vec<UserEntity>> {
    let connection = PgConnection::establish(&database_url)
      .expect("Error connecting to database!");
    Json(users.load::<UserEntity>(&connection)
      .expect("Error loading users"))
}

Сега за получаване на отделни потребители. Още веднъж ще увием отговора в JSON. Но този път ще върнем незадължителен, единичен потребител. Ще използваме параметър за динамично улавяне в URL адреса за User ID.

#[get("/users/<uid>")]
fn fetch_user(database_url: State<String>, uid: i32)
  -> Option<Json<UserEntity>> {
    let connection = ...;
    ...
}

Ще искаме да филтрираме таблицата с потребители по ID. Това ще ни даде списък с различни резултати. Искаме да посочим този вектор като променлив. Защо? В крайна сметка искаме да върнем първия потребител. Но правилата за памет на Rust означават, че трябва или да копираме, или да преместим този елемент. И ние не искаме да преместим нито един елемент от вектора, без да преместим целия вектор. Така че ще премахнем изцяло главата от вектора, което изисква променливост.

#[get("/users/<uid>")]
fn fetch_user(database_url: State<String>, uid: i32)
  -> Option<Json<UserEntity>> {
    let connection = ...;
    use rust_web::schema::users::dsl::*;
    let mut users_by_id: Vec<UserEntity> =
          users.filter(id.eq(uid))
            .load::<UserEntity>(&connection)
            .expect("Error loading users");
    ...
}

Сега можем да направим нашия анализ на случая. Ако списъкът е празен, връщаме None. В противен случай ще премахнем потребителя от вектора и ще го обвием.

#[get("/users/<uid>")]
fn fetch_user(database_url: State<String>, uid: i32) -> Option<Json<UserEntity>> {
    let connection = ...;
    use rust_web::schema::users::dsl::*;
    let mut users_by_id: Vec<UserEntity> =
          users.filter(id.eq(uid))
          .load::<UserEntity>(&connection)
          .expect("Error loading users");
    if users_by_id.len() == 0 {
        None
    } else {
        let first_user = users_by_id.remove(0);
        Some(Json(first_user))
    }
}

Създаване/Актуализиране/Изтриване

Надяваме се, че можете да видите модела сега! Всички наши запитвания са доста прости. Така че всички наши крайни точки следват подобен модел. Свържете се с базата данни, стартирайте заявката и обвийте резултата. Можем да проследим този процес за останалите три крайни точки в основна настройка на CRUD. Нека започнем с „Създаване“:

#[post("/users/create", format="application/json", data = "<user>")]
fn create_user(database_url: State<String>, user: Json<User>)
  -> Json<i32> {
    let connection = ...;
    let user_entity: UserEntity = diesel::insert_into(users::table)
        .values(&*user)
        .get_result(&connection).expect("Error saving user");
    Json(user_entity.id)
}

Както обсъдихме миналата седмица, можем да използваме data заедно с Json, за да посочим данните от формуляра в нашата заявка за публикуване. Дереферираме потребителя с *, за да го извадим от JSON обвивката. След това вмъкваме потребителя и обвиваме неговия ID, за да го изпратим обратно.

Изтриването на потребител също е лесно. Той има същия динамичен път като извличането на потребител. Вместо това просто правим delete повикване в нашата база данни.

#[delete("/users/<uid>")]
fn delete_user(database_url: State<String>, uid: i32) -> Json<i32> {
    let connection = ...;
    use rust_web::schema::users::dsl::*;
    diesel::delete(users.filter(id.eq(uid)))
      .execute(&connection)
      .expect("Error deleting user");
    Json(uid)
}

Актуализирането е последната крайна точка, която приема put заявка. Механиката на крайната точка е точно като другите ни крайни точки. Използваме динамичен компонент на пътя, за да получим идентификатора на потребителя и след това предоставяме Userbody с актуализираните стойности на полето. Единственият трик е, че трябва малко да разширим знанията си за дизела. Ще използваме update и set, за да променим отделни полета на елемент.

#[put("/users/<uid>/update", format="json", data="<user>")]
fn update_user(
  database_url: State<String>, uid: i32, user: Json<User>)
   -> Json<UserEntity> {
    let connection = ...;
    use rust_web::schema::users::dsl::*;
    let updated_user: UserEntity =   
          diesel::update(users.filter(id.eq(uid)))
            .set((name.eq(&user.name),
                  email.eq(&user.email),
                  age.eq(user.age)))
            .get_result::<UserEntity>(&connection)
            .expect("Error updating user");
    Json(updated_user)
}

Другата грешка е, че трябва да използваме препратки (&) за низовите полета във входния потребител. Но сега можем да добавим тези маршрути към нашия сървър и той ще манипулира нашата база данни по желание!

Заключение

Все още има много неща, които можем да подобрим тук. Например, ние все още използваме .expect на много места. От гледна точка на уеб сървър, трябва да улавяме тези проблеми и да ги опаковаме с „Err 500“. Rocket също предоставя някои добри механики за коригиране на това. Следващата седмица обаче ще се насочим към друг сървърен проблем, който Rocket решава умело: удостоверяване. Трябва да ограничим определени крайни точки до определени потребители. Rust предоставя схема за удостоверяване, която е добре кодирана в системата от типове!

За по-задълбочено въведение в Rust гледайте нашия Rust Video Tutorial. Ще ви преведе през много ключови умения като разбиране на паметта и използване на Cargo!