1
\$\begingroup\$

I just started learning Rust, and try to implement a function like Django's get_or_create.

Now my implementation looks too verbose[ I hope that rust can be neater. Therefore, how can I implement this function in less verbose way? There is probably a way to shorten nested match constructions below?

use log::warn;

use diesel::prelude::*;
use diesel::result;

use crate::db::{get_connection, PgPool};
use crate::models::{NewUser, User};

pub fn get_or_create_user(pool: &PgPool, email: &str) -> User {
    use crate::schema::users;

    let new_user = NewUser { email };
    let mut conn = get_connection(pool);

    let result = diesel::insert_into(users::table)
        .values(&new_user)
        .get_result(&mut conn);

    match result {
        Ok(user) => return user,
        Err(err) => match err {
            result::Error::DatabaseError(err_kind, info) => match err_kind {
                result::DatabaseErrorKind::UniqueViolation => {
                    warn!(
                        "{:?} is already exists. Info: {:?}. Skipping.",
                        new_user, info
                    );
                    // another query to DB to get existing user by email
                    let user = user_by_email(pool, new_user.email);
                    return user;
                }
                _ => {
                    panic!("Database error: {:?}", info);
                }
            },
            _ => {
                // TODO: decide how to deal with unexpected errors
                return User {
                    id: 0,
                    email: "".into(),
                };
            }
        },
    }
}

pub fn user_by_email(pool: &PgPool, user_email: &str) -> User {
    use crate::schema::users::dsl::*;

    let mut conn = get_connection(pool);

    let user = crate::schema::users::dsl::users
        .filter(email.eq(user_email))
        .first(&mut conn)
        .unwrap();
    return user;
}
\$\endgroup\$

1 Answer 1

0
\$\begingroup\$

welcome to the Rust community!

get or create

You may indeed make code readable by replacing multiple nested matches with a single match that has nested patterns.

For example, we match on Err(DatabaseError(UniqueViolation, info)) and that grabs all errors that contain a DatabaseError variant of Diesel Error enum, with inner UniqueViolation variant of DatabaseErrorKind. We bind the second value within DatabaseError to info, so we can print the info super easy too. If, for example, the Error is something else than UniqueViolation, we fall through to the next match arm.

The pattern sublanguage is like a language within a language -- you have to learn it and build your intuition about it.

The result of our effort is super readable:

match result {
    Ok(user) => return user,
    Err(DatabaseError(UniqueViolation, info)) => {
        warn!(
            "{:?} is already exists. Info: {:?}. Skipping.",
            new_user, info
        );
        // another query to DB to get existing user by email
        user_by_email(new_user.email)
    }
    Err(DatabaseError(_, info)) => {
        panic!("Database error: {:?}", info);
    }
    _ => {
        // TODO: decide how to deal with unexpected errors
        User {
            id: 0,
            email: "".into(),
        }
    }
}

I had an idea that you may only build one query, which would use ON CONFLICT, and kill two birds with one stone. Unfortunately, Diesel dsl does not seem to support ON CONFLICT (...) DO NOTHING RETURNING *.

Other concerns

Syntax nitpick:

let user = schema::users::dsl::users
    .filter(email.eq(user_email))
    .first(&mut conn)
    .unwrap();
return user;

You may just return the user value directly, replacing the above code with this:

schema::users::dsl::users
    .filter(email.eq(user_email))
    .first(&mut conn)
    .unwrap()

Result

The result is available on my github: https://github.com/pczarn/codereview/tree/81d3fcddd3921bf1b4df4bb347be5dcad3de743f/2022/9/get_or_create

I cleaned up your code, migrated to sqlite for local testing and this is what I got:

extern crate diesel;

mod schema;

use diesel::sqlite::SqliteConnection;
use diesel::prelude::*;
use dotenvy::dotenv;
use std::env;

use log::warn;

use diesel::prelude::*;

use schema::users;

#[derive(Debug, Insertable)]
#[diesel(table_name = users)]
struct NewUser<'a> {
    email: &'a str,
}

#[derive(Queryable)]
pub struct User {
    id: i32,
    email: String,
}

pub fn get_or_create_user(email: &str) -> User {
    use diesel::result::{Error::DatabaseError, DatabaseErrorKind::UniqueViolation};

    let new_user = NewUser { email };
    let mut conn = get_connection();

    let result = diesel::insert_into(users::table)
        .values(&new_user)
        .get_result(&mut conn);

    match result {
        Ok(user) => return user,
        Err(DatabaseError(UniqueViolation, info)) => {
            warn!(
                "{:?} is already exists. Info: {:?}. Skipping.",
                new_user, info
            );
            // another query to DB to get existing user by email
            user_by_email(new_user.email)
        }
        Err(DatabaseError(_, info)) => {
            panic!("Database error: {:?}", info);
        }
        _ => {
            // TODO: decide how to deal with unexpected errors
            User {
                id: 0,
                email: "".into(),
            }
        }
    }
}

pub fn user_by_email(user_email: &str) -> User {
    use schema::users::dsl::*;

    let mut conn = get_connection();

    let user = schema::users::dsl::users
        .filter(email.eq(user_email))
        .first(&mut conn)
        .unwrap();
    return user;
}

pub fn get_connection() -> SqliteConnection {
    dotenv().ok();

    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    SqliteConnection::establish(&database_url)
        .unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}

fn main() {
    get_or_create_user("[email protected]");
    get_or_create_user("[email protected]");
}
\$\endgroup\$
1
  • \$\begingroup\$ Thank you for so detailed explanation! I definitely need time to realize how to apply matches with all nested matches, unwrap*(), ?, ok() and so on ) Your answer helped me a lot. \$\endgroup\$
    – Aleksey
    Commented Sep 21, 2022 at 6:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.