Rust web 开发-2. 数据库/orm

2023/11/17 17:50 下午 posted in  rust Rust web

本文介绍第二部分,数据库及 orm。
主要介绍 Rust 如何使用 sqlx 连接 MySQL,orm 选用了 rbatis。

准备 MySQL 数据库,简单的一个表结构

CREATE TABLE `user` (
	`id` bigint NOT NULL AUTO_INCREMENT,
	`user_name` varchar(32) DEFAULT NULL,
	`password` varchar(64) DEFAULT NULL,
	PRIMARY KEY (`id`)
);

接下来分别使用 sqlx 和 rbatis 进行 CRUD 操作。

sqlx

添加依赖

sqlx = { version = "0.7.2", features = [
    "runtime-async-std-native-tls",
    "mysql",
    "chrono",
] }

sqlx 连接数据库的链接格式:mysql://{user}:{password}@{IP}:{port}/{database name}
例如我本地的数据库地址:mysql://root:12345678@localhost:3306/rust_web

连接 MySQL

use sqlx::{mysql::MySqlPoolOptions, MySql, Pool};

pub async fn get_mysql_pool() -> Pool<MySql> {
    let database_url = "mysql://root:12345678@localhost:3306/rust_web";
    MySqlPoolOptions::new().connect(database_url).await.unwrap()
}

api

use actix_web::{get, post, web, HttpResponse, Scope};
use serde::{Deserialize, Serialize};

use crate::{db::get_mysql_pool, success};

// 定义路由,对外暴露的接口
pub fn routes() -> Scope {
    web::scope("/users")
        .service(add_user)
        .service(query_user)
        .service(update_user)
        .service(delete_user)
}

// 定义实体
#[derive(Debug, Deserialize, Clone, Serialize, Default,sqlx::FromRow)]
#[serde(rename_all = "camelCase")]
pub struct User {
    pub id: Option<i64>,
    pub user_name: Option<String>,
    pub password: Option<String>,
}

#[post("/add")]
pub async fn add_user(user: web::Json<User>) -> HttpResponse {
    let user = user.to_owned();
    let id = save_user(user).await.unwrap();
    success(Some(id))
}
// insert
async fn save_user(user: User) -> Result<u64, sqlx::Error> {
    // 获取数据库连接池
    let pool = get_mysql_pool().await;
    let sql = "insert into user(user_name, password) values (?,?)";
    let result = sqlx::query(sql)
        .bind(user.user_name)
        .bind(user.password)
        .execute(&pool)
        .await?;
    let id = result.last_insert_id();
    Ok(id)
}

// update
#[post("/update/{id}")]
pub async fn update_user(id: web::Path<i64>, user: web::Json<User>) -> HttpResponse {
    let user = user.to_owned();

    let sql = "update user set user_name =?, password =? where id = ?";
    sqlx::query(sql)
        .bind(user.user_name)
        .bind(user.password)
        .bind(id.into_inner())
        .execute(&get_mysql_pool().await)
        .await.unwrap();
    success(Some(1))
}

// select
#[get("/query/{id}")]
pub async fn query_user(id:web::Path<i64>) -> HttpResponse {
    let sql = "select * from user where id = ?";
    let result = sqlx::query_as::<_,User>(sql).bind(id.into_inner()).fetch_optional(&get_mysql_pool().await).await.unwrap();
    success(result)
}

// delete
#[post("/delete/{id}")]
pub async fn delete_user(id:web::Path<i64>) -> HttpResponse {
    let sql = "delete from user where id = ?";
    sqlx::query(sql).bind(id.into_inner()).execute(&get_mysql_pool().await).await.unwrap();
    success(Some(1))
}

服务启动之后测试:
新增 user:

curl -X "POST" "http://localhost:8099/users/add" \
     -H 'token: 1' \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{
  "password": "3456789",
  "userName": "张三"
}'

查询 user:

curl "http://localhost:8099/users/query/1" \
     -H 'token: 1'

修改 user:

curl -X "POST" "http://localhost:8099/users/update/1" \
     -H 'token: 1' \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{
  "password": "987654",
  "userName": "lisi"
}'

删除 user:

curl -X "POST" "http://localhost:8099/users/delete/2" \
     -H 'token: 1'

orm

orm 目前我使用过的有两个,一个是 Diesel,一个是 rbatis。

Diesel 可以看以前写过的一篇文章,https://blog.hitol.top/16669473309292.html
本篇文章主要介绍一下 rbatis。

添加依赖

# rbatis
rbs = { version = "4.3" }
rbatis = { version = "4.3" }
rbdc-mysql = { version = "4.3" }

创建 rbatis

use rbatis::RBatis;
use rbdc_mysql::MysqlDriver;

pub async fn get_rb() -> RBatis {
    let database_url = "mysql://root:12345678@localhost:3306/rust_web";
    let rb = RBatis::new();
    rb.init(MysqlDriver {}, database_url).unwrap();
    rb
}

在 User 实体定义的文件中调用 crud! 宏。

crud!(User {});//crud = insert+select_by_column+update_by_column+delete_by_column

宏调用之后就可以直接使用 User::insert、User::update、User::select、User::delete。

#[derive(Debug, Deserialize, Clone, Serialize, Default, sqlx::FromRow)]
#[serde(rename_all = "camelCase")]
pub struct User {
    pub id: Option<i64>,
    pub user_name: Option<String>,
    pub password: Option<String>,
}

crud!(User {}); // crud

#[post("/add")]
pub async fn add_user(user: web::Json<User>) -> HttpResponse {
    let user = user.to_owned();
    // let id = save_user(user).await.unwrap();
    let id = save_user_rb(user).await;
    success(Some(id))
}

async fn save_user_rb(user: User) -> i64 {
    let rbatis = get_rb().await;
    let x = User::insert(&rbatis, &user).await;
    let x = x.unwrap();
    i64::from(x.last_insert_id)
}

//update: User::update_by_column(&get_rb().await, &user, "id").await;
//select: User::select_by_column(&get_rb().await, "id",id.into_inner()).await.unwrap();
//delete: User::delete_by_column(&get_rb().await, "id", id.into_inner()).await;

使用了 serde::rename_all 将实体中的字段格式从下划线转为了驼峰类型,然而使用 rbaits 会有问题,打印出的 sql 中字段也是驼峰的类型。需要注意的是本文中代码仅为演示使用,偷了个懒,数据库实体跟 VO 层用的是一个 struct 实体。在实际生产代码中应是两个实体,通过实现 From/into 转换为数据库实体即可。

数据库连接池初始化

需要注意点是数据库连接池现在是每次调用都会初始化,为了演示连接数据库。要改为只初始化一次,可以使用lazy_static宏来延迟初始化静态变量。lazy_static宏允许在首次访问变量时执行初始化代码。

use lazy_static::*;
use rbatis::RBatis;
use rbdc_mysql::MysqlDriver;
use sqlx::{mysql::MySqlPoolOptions, MySql, Pool};
use std::{sync::Mutex, time::Duration};

lazy_static! {
    static ref MYSQL_POOL: Mutex<Vec<Pool<MySql>>> = Mutex::new(vec![]);
}

lazy_static! {
    static ref RB: Mutex<RBatis> = Mutex::new(RBatis::new());
}

pub async fn init_db(database_url: String) {
    let pool = MySqlPoolOptions::new()
        .max_connections(20)
        .min_connections(5)
        .idle_timeout(Some(Duration::from_secs(30)))
        .connect(&database_url)
        .await
        .unwrap();

    let mut pools = MYSQL_POOL.lock().unwrap();
    (*pools).push(pool);

    let rbatis = RBatis::new();
    rbatis.init(MysqlDriver {}, &database_url).unwrap();
    let mut rb = RB.lock().unwrap();
    *rb = rbatis;

}

pub async fn get_mysql_pool() -> Pool<MySql> {
    let pools = MYSQL_POOL.lock().unwrap();
    unsafe { (*pools).get_unchecked(0).to_owned() }
}

pub async fn get_rb() -> RBatis {
    RB.lock().unwrap().clone()
}

本篇文章代码在 github

我在项目中用的话一般是 sqlx + rbatis 一起使用,简单的 sql 用 rbatis 自动生成的,复杂一些的 sql 就使用 sqlx 自己手写……