LoginSignup
5
0

More than 3 years have passed since last update.

Rust - A beginner's take

Last updated at Posted at 2020-12-16

Introduction

Disclaimer: Author coming from a frontend background and with very limited backend exposure. However a couple of months ago he decided to take a dive into the Rust world and thought it wouldn't be a very bad idea to share with you all his first little discoveries :)

So as it probably becomes quite obvious from the disclaimer in this article I would like to share with you some interesting challenges I 've come to face during these first 2 months working with Rust which I believe might be helpful for people who are just setting off on their Rust journey. I would like to think that even people with more experience with the language will find some of these insights not completely trivial.

My idea is to walk you through some simple examples, where we can use different features of the language but also talk a little bit about the thinking process behind the implementation. Before going through the examples I should probably mention that we are going to use diesel (http://diesel.rs/) for everything that involves fetching/updating/inserting records to our Postgres database and I highly recommend everyone to check this guide first http://diesel.rs/guides/getting-started/ if they would also like to code along while reading through the examples that follow.

Challenge #1

Alright and now that all the disclaimers are out let's start with our first example. So the challenge here goes something like this. Let's say we have a users table and for each user we also store their password (among other fields). There can only be one valid password per user so every time a user tries to update their password we need to invalidate the previous valid one and add a new record for this user with the new password. We also have another field called pass_count which essentially counts how many times a user has changed their password. This means that every time we add a new password for a user we need to check also the pass_count value stored together with the to-be-invalidated password and when adding the new password increment the pass_count value by 1.

The reason why we don't directly overwrite the previous record for the same user is that we want to be able to trace back all the times that this user tried to update their password and have a record of all the passwords that they have used.

Setting up the project with diesel

Ok so before jumping to the implementation, I will try to create here a simple project with all the setup that we need to work with. Please feel free to follow these steps as well or you can also just go and check the implementation directly. The process for creating our demo project is very similar to the one described in this guide http://diesel.rs/guides/getting-started/ so instead of repeating here all those steps again, I will just list down the changes that we need to make, to tailor it to our needs.

  • Since we are going to work with a users table when creating the migration run:
diesel migration generate create_users
  • The users table inside our up.sql file should look like:

up.sql

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR NOT NULL,
    password VARCHAR NOT NULL,
    status_flg BOOLEAN NOT NULL,
    pass_count INTEGER NOT NULL
)
  • Also inside our down.sql file we should add:

down.sql

DROP TABLE users
  • The code inside the schema.rs file is auto-generated after running diesel migration run and should look like this:

src/schema.rs

table! {
    users (id) {
        id -> Int4,
        username -> Varchar,
        password -> Varchar,
        status_flg -> Bool,
        pass_count -> Int4,
    }
}  
  • Now let's create our models.rs file and inside there let's add the structs we are going to work with for this example:

src/models.rs

use super::schema::users;

#[derive(Queryable, Debug)]
pub struct User {
    pub id: i32,
    pub username: String,
    pub password: String,
    pub status_flg: bool,
    pub pass_count: i32,
}

#[derive(Insertable)]
#[table_name=users]
pub struct NewUserEntry {
    pub username: String,
    pub password: String,
    pub status_flg: bool,
    pub pass_count: i32,
}
  • Finally in order to have a nice structure for our project, under src directory let's create a main.rs file where we 're going to put our main() function as well as an api directory inside which we 're going to put our api that incorporates the logic described above. Inside the api directory let's create a new file with our api - let's call it update_password.rs - and a mod.rs file that makes our api visible to external crates:

src/api/mod.rs

pub mod update_password;

Implementing the update_password api

Now we are finally ready to implement our update_password.rs api and our main.rs that calls it. Let me first post the code here and I will further explain some of this code right after.

src/main.rs

use anyhow::Result; // add anyhow to your dependencies in Cargo.toml
mod api;

fn main() -> Result<()> {
    let username = String::from("username");
    let new_password = String::from("new_password"); // Friendly reminder: This is just for demonstration purposes
    api::update_password::run(username, new_password)?; // here we call our api
    Ok(())
}

src/api/update_password.rs

extern crate diesel_demo;
extern crate diesel;
use self::diesel_demo::*;
use self::diesel::prelude::*;
use diesel_demo::models::*;
use diesel_demo::schema::users;
use anyhow::Result;

pub fn run(username: String, password: String) -> Result<()> {
    let conn = establish_connection();
    let query = users::table.filter( // constructs the query
        users::username.eq(username.clone()).and(users::status_flg.eq(true))
    );
    let new_pass_count = diesel::update(query)
        .set(users::status_flg.eq(false)) // here we specify which columns we want to update
        .returning(users::pass_count) 
        .get_result::<i32>(&conn) // if used without the returning clause would return the whole updated record - here it returns only the pass_count value
        .optional()? // we use optional cause we cannot be sure if the record exists or not
        .map_or_else(
            || 1, // we set the pass_count to 1 if record not found
            |num| num + 1, // the new pass_count value should be the current pass_count value + 1
        );
    let new_entry = NewUserEntry { // a different struct is used for inserting a new user entry
        username,
        password,
        status_flg: true, 
        pass_count: new_pass_count,
    };
    diesel::insert_into(users::table).values(&new_entry).execute(&conn)?;
    Ok(())
}

get_result + returning combo

Ok so looking more carefully into the update_password implementation we can see some of the powerful features we get by using diesel with Postgres in this case. Here since we are only interested in the pass_count value, we can use the get_result api along with the returning clause when executing the UPDATE operation to invalidate the user's current password, so that we get back only the pass_count value instead of the whole record. Also since we are not sure whether the record exists or not, we can use .optional() on the result and chain it with map_or_else so that we set the new_pass_count value directly:

  • 1, if there is no record for that user yet
  • pass_count + 1, if the record was found

Now that we have the new_pass_count value stored, we can insert a new record with the new password for that user. Notice that for the INSERT operation a different NewUserEntry struct is used (also previously defined by us in the model.rs file).

build_transaction

Ok seems like we 're all set..we invalidated the previous password for that user and our user now can continue using our services with their new password! Well... It's true that our code now works as expected, however having our api handling an UPDATE and an INSERT operation in this way is not very safe and that's because a different service might try to access our database right after the UPDATE operation is finished and before the INSERT operation takes place, leading to unpredictable behavior. To prevent such a scenario we need to wrap those two operations in a transaction and make the whole thing atomic. Let's edit our code so that we guard our api against such funky outcomes:


pub fn run(username: String, password: String) -> Result<()> {
    let conn = establish_connection();
    conn.build_transaction().repeatable_read().run(|| {
        let query = users::table.filter( // constructs the query
            users::username.eq(username.clone()).and(users::status_flg.eq(true))
        );
        ...
        diesel::insert_into(users::table).values(&new_entry).execute(&conn)?;    
    })

    Ok(())
}

As we see, this can be done in one line with diesel conn.build_transaction().repeatable_read().run(|| {...}) , where we build the transaction specifying different parameters (like in this case that we set REPEATABLE READ as the ISOLATION LEVEL) and we call run on the builder to run the code wrapped in the transaction. Here basically everything related to the two operations (UPDATE and INSERT) becomes part of one bigger transaction.

Alright and now seems like we got all the bases covered, hopefully with no caveats this time. And actually I just realized that this article is getting too long, so better stop here for now. We 'll continue with more examples next time(?) Jaaa mata ne!

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0