多对多与 Join 实体
多对多关系在数据库层面需要一张**中间表(Join 实体)**。`rust-ef` 支持两种声明方式。
方式一:显式 Join 实体 + 泛型参数
#[derive(Debug, Clone, EntityType)]
#[table("students")]
pub struct Student {
#[primary_key]
#[auto_increment]
pub student_id: i32,
pub name: String,
#[navigation]
pub courses: HasMany<Course, Enrollment>,
}
#[derive(Debug, Clone, EntityType)]
#[table("courses")]
pub struct Course {
#[primary_key]
#[auto_increment]
pub course_id: i32,
pub title: String,
}
// Join 实体
#[derive(Debug, Clone, EntityType)]
#[table("enrollments")]
pub struct Enrollment {
#[primary_key]
#[auto_increment]
pub enrollment_id: i32,
#[foreign_key(Student)]
pub student_id: i32,
#[foreign_key(Course)]
pub course_id: i32,
}
`HasMany<Course, Enrollment>` 告诉框架:加载 `Student.courses` 时,通过 `Enrollment` 表进行关联查询。
方式二:`#[through]` 属性
#[derive(Debug, Clone, EntityType)]
#[table("students")]
pub struct Student {
#[primary_key]
#[auto_increment]
pub student_id: i32,
pub name: String,
#[navigation]
#[through(Enrollment)]
pub courses: HasMany<Course>,
}
两种方式功能等价,选择团队更喜欢的风格即可。
查询多对多
// 通过 linq! 的 include 子句预加载多对多导航
let students = linq!(ctx.set::<Student>(); include s.courses)
.to_list()
.await?;
// courses 已通过双查询策略自动物化
assert!(students[0].courses.len() > 0);
按主键查找
// 单主键:find 使用实体 PK 元数据,不再硬编码 "id"
let student = ctx.set::<Student>().query().find(1).await?;
// 复合主键:find_by_key 接收列名常量 + 值数组
use rust_ef::provider::DbValue;
let enrollment = ctx
.set::<Enrollment>()
.query()
.find_by_key(&[
(Enrollment::COLUMN_STUDENT_ID, DbValue::I32(1)),
(Enrollment::COLUMN_COURSE_ID, DbValue::I32(2)),
])
.await?;
设计要点
| 实践 | 说明 | |------|------| | Join 实体也需 `#[derive(EntityType)]` | 它需要有自己的表映射和主键 | | Join 实体主键建议独立 | 不要只用 `student_id + course_id` 作为复合主键,便于后续扩展(如加入 `enrolled_at`) |
下一节:[Eager Loading:Include 与 ThenInclude](eager-loading.md)