table name "staff" specified more than once
主线位置:定义主线上的
RelationDef被查询主线的join读取的那一步。join(jt, rel)把rel.to_tbl原样放进JOIN,ON条件也用表名;两端是同一张表时,SQL 里就有两个都叫staff的表,Postgres 在解析阶段拒绝。RelationDef和自引用的写法见关系的「自引用:同一张表出现两次」,join 系列方法见 join 与 find_related。
flowchart TB
rel["ReportsTo: staff 到 staff"] --> j{"怎么 join"}
j -->|join| plain["JOIN staff ON staff.x = staff.id"]
plain --> err["42712"]
j -->|"join_as mgr"| a1["JOIN staff AS mgr"]
j -->|"Linked"| a2["JOIN staff AS r0"]
a1 --> ok["正常执行"]
a2 --> ok
本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行,依赖 sea-orm = "=2.0.4",对照组 =1.1.20。表:
CREATE TABLE staff (id serial PRIMARY KEY, name text NOT NULL, reports_to_id int REFERENCES staff(id));
INSERT INTO staff (name, reports_to_id) VALUES ('boss', NULL), ('alice', 1), ('bob', 1);
实体是 dense 写法,reports_to 用 self_ref 声明,宏生成 Relation::ReportsTo(belongs_to = "Entity",reports_to_id 到 id):
#[sea_orm(
self_ref,
relation_enum = "ReportsTo",
relation_reverse = "Manages",
from = "reports_to_id",
to = "id"
)]
pub reports_to: BelongsTo<Option<Entity>>,
#[sea_orm(self_ref, relation_enum = "Manages", relation_reverse = "ReportsTo")]
pub manages: HasMany<Entity>,
现象
src/bin/selfjoin.rs 的第一段,想列出每个员工和他的上级:
let r = staff::Entity::find().join(JoinType::LeftJoin, staff::Relation::ReportsTo.def()).all(&db).await;
--- 1. join(LeftJoin, ReportsTo)
INFO sqlx::query: summary="SELECT \"staff\".\"id\", \"staff\".\"name\", \"staff\".\"reports_to_id\" …" db.statement="\n\nSELECT \"staff\".\"id\", \"staff\".\"name\", \"staff\".\"reports_to_id\" FROM \"staff\" LEFT JOIN \"staff\" ON \"staff\".\"reports_to_id\" = \"staff\".\"id\"\n" rows_affected=0 rows_returned=0 elapsed=1.254916ms elapsed_secs=0.001254916
Query(SqlxError(Database(PgDatabaseError { severity: Error, code: "42712", message: "table name \"staff\" specified more than once", detail: None, hint: None, position: None, where: None, schema: None, table: None, column: None, data_type: None, constraint: None, file: Some("parse_relation.c"), line: Some(463), routine: Some("checkNameSpaceConflicts") })))
ON "staff"."reports_to_id" = "staff"."id" 本身就分不清两边,Postgres 在解析阶段(checkNameSpaceConflicts)就拒绝了。sea-orm 在构造时不检查,错误要到语句发出去才出现。
不是自引用也会撞上:一张表有两个外键指向同一张表,两个关系各 join 一次(src/bin/join_twice.rs,doc.author_id 和 doc.reviewer_id 都指向 users):
let r = doc::Entity::find()
.join(JoinType::InnerJoin, doc::Relation::Author.def())
.join(JoinType::InnerJoin, doc::Relation::Reviewer.def())
.all(&db)
.await;
join + join : Query Error: error returned from database: table name "users" specified more than once at line 463
本站用的 1.1.20(exp-seaorm-pit1-old/src/bin/selfjoin_old.rs,展开写法的实体,belongs_to = "Entity"):
Query Error: error returned from database: table name "staff" specified more than once
出处
fn join(mut self, join: JoinType, rel: RelationDef) -> Self {
self.query().join(join, rel.to_tbl.clone(), rel);
self
}
JOIN 的表就是 rel.to_tbl,ON 条件由 From<RelationDef> for Condition 生成:两端各自「有别名用别名,没有就用表名」。自引用的 RelationDef 两端都是 staff、都没有别名,FROM 里已经有一个 staff,于是 SQL 里出现两个同名的表。sea-orm 不会替你起别名,因为它不知道你想让哪一个叫什么:后面的 select、filter 要用这个名字去引用列。
find_linked 的 r0、r1 是为自引用加的#163(「find_linked() generates invalid SQL for self referencing associations」)报告:「The implementation of find_linked() does not use aliases on join statements, so for self referencing many to many associations such as TABLE -> RELATES_TO -> TABLE the generated query is invalid.」讨论里有人提议给 RelationDef 加一个 alias: Option<String> 字段,得到的回答是「If you are talking RelationDef, then no, not a good idea.」。最后的修法 #182(「find_linked join with table alias」,首次发布于 0.2.4)没有改 RelationDef,而是让 find_linked 每一跳一律用 r{i} 作别名,也就是今天 find_linked 里的 format!("r{i}")。普通的 join 从那时起到 2.0.4 都没有自动别名,要别名得自己调 join_as。
修法
join_as 起别名。 join_as 把 to_tbl 换成带别名的版本,ON 条件随之改用别名。别名那一侧的列要自己选出来,按名字引用:
let rows: Vec<(String, Option<String>)> = staff::Entity::find()
.join_as(JoinType::LeftJoin, staff::Relation::ReportsTo.def(), "mgr")
.select_only()
.column(staff::Column::Name)
.column_as(Expr::col(("mgr", staff::Column::Name)), "manager")
.order_by_asc(staff::Column::Id)
.into_tuple()
.all(&db)
.await?;
--- 2. join_as(LeftJoin, ReportsTo, "mgr")
INFO sqlx::query: summary="SELECT \"staff\".\"name\", \"mgr\".\"name\" AS …" db.statement="\n\nSELECT \"staff\".\"name\", \"mgr\".\"name\" AS \"manager\" FROM \"staff\" LEFT JOIN \"staff\" AS \"mgr\" ON \"staff\".\"reports_to_id\" = \"mgr\".\"id\" ORDER BY \"staff\".\"id\" ASC\n" rows_affected=3 rows_returned=3 elapsed=2.688125ms elapsed_secs=0.002688125
[("boss", None), ("alice", Some("boss")), ("bob", Some("boss"))]
两个外键指向同一张表的情况一样,每个 join 一个别名:
join_as + join_as: [(1, "ann", "bob")]
写一个 Linked,用 find_also_linked 拿回两个 Model。 自引用不生成 Related<Entity> for Entity,find_also_related 用不了;Linked 的每一跳自动起别名:
pub struct ManagerLink;
impl Linked for ManagerLink {
type FromEntity = Entity;
type ToEntity = Entity;
fn link(&self) -> Vec<RelationDef> {
vec![Relation::ReportsTo.def()]
}
}
// ...
let rows = staff::Entity::find().find_also_linked(staff::ManagerLink).order_by_asc(staff::Column::Id).all(&db).await?;
--- 3. find_also_linked(ManagerLink)
INFO sqlx::query: summary="SELECT \"staff\".\"id\" AS \"A_id\", …" db.statement="\n\nSELECT \"staff\".\"id\" AS \"A_id\", \"staff\".\"name\" AS \"A_name\", \"staff\".\"reports_to_id\" AS \"A_reports_to_id\", \"r0\".\"id\" AS \"B_id\", \"r0\".\"name\" AS \"B_name\", \"r0\".\"reports_to_id\" AS \"B_reports_to_id\" FROM \"staff\" LEFT JOIN \"staff\" AS \"r0\" ON \"staff\".\"reports_to_id\" = \"r0\".\"id\" ORDER BY \"staff\".\"id\" ASC\n" rows_affected=3 rows_returned=3 elapsed=801.917µs elapsed_secs=0.000801917
boss -> None
alice -> Some("boss")
bob -> Some("boss")
find_also_linked 是 LEFT JOIN,右边那张 staff 叫 r0,列加 B_ 前缀拆成第二个 Model。要在条件里引用上级的列,写 Expr::col(("r0", ..))。
怎么确认没中招
- 找所有
join(/join_rev(的调用,看它的RelationDef两端是不是同一张表(belongs_to = "Entity"、self_ref),或者同一条查询里有没有两个 join 指向同一张表。有就要换成join_as/join_as_rev。 - 这个错误只在语句发出去时出现,最便宜的检查是给每个这样的查询写一条连测试库的测试;
MockDatabase不解析 SQL,测不出来(见 Mock 与 Proxy)。 - 本站:按固定提交
89a61b1在crates/下没有join_as、Linked,唯一的关联查询是 comments 里的find_also_related(user::Entity),两端是不同的表,不受影响。
小结
join(jt, rel)用rel.to_tbl原样 join,ON条件用表名;自引用或同一张表 join 两次时 SQL 里有两个同名表,Postgres 报42712 table name "staff" specified more than once。2.0.4 和 1.1.20 一样。- sea-orm 在构造时不检查,错误到执行时才出现。
- 修法:
join_as(jt, rel, "别名")并按别名选列;或写一个Linked,find_also_linked/find_linked自动用r0、r1(#182,0.2.4 起)。