Rust in Everything
基于 sea-orm 2.0.4

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

出处

QuerySelect::join:

    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 要用这个名字去引用列。

📝NOTE
冷知识: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 起)。

💬 关联讨论

发起讨论
加载中...