如何使用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_id和team_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结构体需要的id和name两个值——Decode<Team>只能把单个列的值转换成Team,但单个整数ID没法构造出包含name的Team,所以这条路从一开始就行不通。
总结一下:SQLx不是ORM,不会自动帮你做关联查询和嵌套映射,但只要你先让数据库返回所有需要的数据,再通过中间转换或自定义FromRow,就能轻松实现你想要的功能!




