You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何使用Rust SQLx通过ID查询/存储包含关联结构体的数据(以Employee与Team为例)

如何使用Rust SQLx通过ID查询/存储包含关联结构体的数据(以Employee与Team为例)

我完全懂你这种 frustration!在Python里用SQLAlchemy这类ORM能轻松加载关联对象,但SQLx作为无ORM的SQL工具库,确实不会自动帮你做JOIN和嵌套结构体的映射——得我们自己搭个桥,不过其实不用硬写复杂的Decode实现,有两种简单的办法解决你的问题!

核心问题分析

首先得明确:SQLx的query_as默认只会把单一行的列直接映射到结构体的平级字段。你直接SELECT * FROM employees拿到的只有team列的ID值,数据库根本没返回Team的name,所以SQLx自然没法帮你构造出完整的Team结构体。要拿到Team的完整数据,你得先让数据库返回这些数据,再告诉SQLx怎么把结果映射成嵌套结构体。


方案一:JOIN查询 + 手动转换(最推荐,简单直观)

这是最符合SQLx设计思路的方式:先通过SQL JOIN把需要的所有数据查出来,用一个扁平化的中间结构体接收结果,再转换成你想要的嵌套Employee结构体。

步骤1:定义中间结构体

这个结构体要和JOIN查询返回的列完全对应:

// 中间结构体,对应JOIN后的查询结果列
#[derive(Debug, FromRow, PartialEq)]
struct EmployeeTeamJoined {
    pub id: u64,
    pub name: String,
    pub team_id: u64,
    pub team_name: String,
}

步骤2:修改查询与转换逻辑

把原来的SELECT *改成JOIN查询,然后把中间结构体转换成Employee

async fn get_employee() {
    let pool: Pool<Sqlite> = get_pool().await;
    let mut conn = pool.acquire().await.unwrap();

    // 用JOIN查询拿到员工和对应的团队数据
    let joined: EmployeeTeamJoined = query_as(
        r#"
        SELECT 
            e.id, e.name, 
            t.id as team_id, t.name as team_name
        FROM employees e
        JOIN teams t ON e.team = t.id
        WHERE e.id = 1
        "#,
    )
    .fetch_one(&mut *conn)
    .await
    .unwrap();

    // 转换成你需要的嵌套结构体
    let employee = Employee {
        id: joined.id,
        name: joined.name,
        team: Team {
            id: joined.team_id,
            name: joined.team_name,
        },
    };

    dbg!(&employee.team.name); // 现在能正常打印"East Coast Team"了!
}

这种方式的好处是:不需要写任何复杂的trait实现,逻辑清晰,而且完全可控——你能精确控制SQL查询的内容,避免不必要的性能开销。


方案二:自定义FromRow实现(直接用query_as<Employee>

如果你真的想直接用query_as<Employee>,那应该实现FromRow trait而不是Decode——因为Decode是用来处理单个列到单个类型的映射,而FromRow才是处理整行数据到结构体的映射。

实现FromRow for Employee

use sqlx::{Row, sqlite::SqliteRow};

#[derive(Debug, PartialEq)]
pub struct Employee {
    pub id: u64,
    pub name: String,
    pub team: Team,
}

#[derive(Debug, PartialEq)]
pub struct Team {
    pub id: u64,
    pub name: String,
}

// 为Employee实现FromRow,从JOIN后的行中提取数据
impl<'r> FromRow<'r, SqliteRow> for Employee {
    fn from_row(row: &'r SqliteRow) -> Result<Self, sqlx::Error> {
        Ok(Self {
            id: row.try_get("id")?,
            name: row.try_get("name")?,
            team: Team {
                id: row.try_get("team_id")?,
                name: row.try_get("team_name")?,
            },
        })
    }
}

现在可以直接用query_as<Employee>

async fn get_employee_direct() {
    let pool: Pool<Sqlite> = get_pool().await;
    let mut conn = pool.acquire().await.unwrap();

    let employee: Employee = query_as(
        r#"
        SELECT 
            e.id, e.name, 
            t.id as team_id, t.name as team_name
        FROM employees e
        JOIN teams t ON e.team = t.id
        WHERE e.id = 1
        "#,
    )
    .fetch_one(&mut *conn)
    .await
    .unwrap();

    dbg!(&employee.team.name); // 同样正常工作!
}

注意:这个实现依赖于你的查询必须返回team_idteam_name这两个列,否则try_get会返回错误。


补充:存储包含关联结构体的数据

当你要把Employee存入数据库时,只需要把team.id作为外键存入employees表的team列即可:

async fn add_employee(employee: &Employee) {
    let pool: Pool<Sqlite> = get_pool().await;
    let mut conn = pool.acquire().await.unwrap();

    query!(
        r#"
        INSERT INTO employees (id, name, team)
        VALUES (?, ?, ?)
        "#,
        employee.id,
        employee.name,
        employee.team.id
    )
    .execute(&mut *conn)
    .await
    .unwrap();
}

为什么你之前的Decode实现没用?

你之前尝试为Team实现Decode,但问题在于:当你查询SELECT * FROM employees时,数据库返回的team列只是一个整数ID,而不是Team结构体需要的idname两个值——Decode<Team>只能把单个列的值转换成Team,但单个整数ID没法构造出包含nameTeam,所以这条路从一开始就行不通。

总结一下:SQLx不是ORM,不会自动帮你做关联查询和嵌套映射,但只要你先让数据库返回所有需要的数据,再通过中间转换或自定义FromRow,就能轻松实现你想要的功能!

火山引擎 最新活动