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;
}
Toby Speight
87.2k14 gold badges104 silver badges322 bronze badges
asked Sep 9, 2022 at 10:45
\$\endgroup\$

1 Answer 1

1
\$\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]");
}
answered Sep 17, 2022 at 8:09
\$\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\$ Commented Sep 21, 2022 at 6:10

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.