SeaQL / sea-orm

🐚 An async & dynamic ORM for Rust
https://www.sea-ql.org/SeaORM/
Apache License 2.0
7.37k stars 517 forks source link

sea-orm-cli does not generate has_many relation if there is another one-to-one relation #2358

Open emmiegit opened 2 months ago

emmiegit commented 2 months ago

Description

I have two tables in my schema which depend on each other. One of them is site, a "main" table which has many foreign keys pointing to site.site_id. Another is site_domain, which has a one-to-many relationship to sites, that is, one site could have no custom domains, or many. However, sites can (optionally) choose one domain as its "preferred" one, and there is a foreign key constraint from this column in site to site_domain.

The entity file generated for site lacked a Relation case for doing a JOIN where I want the site associated with a particular domain, instead it utilized a different foreign key, which was causing my query to not produce results as expected.

See "background" for further information.

Steps to Reproduce

  1. Create a postgres database.
  2. Apply a schema where, for tables which have a one-to-many relation, the one table has a foreign key constraint to one of the many.
  3. Generate entity files. I used sea-orm-cli generate entity --verbose --database-url [url] --output-dir [dir].
  4. Inspect the generated entity file and observe that there is no result for the many table in Relation which has #[sea_orm(has_many)].

Reproducible Example

A Postgres database with the given schema will produce the following entity files:

CREATE TABLE site (
    site_id BIGSERIAL PRIMARY KEY,
    custom_domain TEXT
);

CREATE TABLE site_domain (
    domain TEXT PRIMARY KEY,
    site_id BIGINT NOT NULL REFERENCES site(site_id)
);

ALTER TABLE site
    ADD CONSTRAINT site_custom_domain_fk
    FOREIGN KEY (custom_domain) REFERENCES site_domain(domain);
Entity file: site.rs ```rs //! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1 use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "site")] pub struct Model { #[sea_orm(primary_key)] pub site_id: i64, #[sea_orm(column_type = "Text", nullable)] pub custom_domain: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( belongs_to = "super::site_domain::Entity", from = "Column::CustomDomain", to = "super::site_domain::Column::Domain", on_update = "NoAction", on_delete = "NoAction" )] SiteDomain, } impl Related for Entity { fn to() -> RelationDef { Relation::SiteDomain.def() } } impl ActiveModelBehavior for ActiveModel {} ```
Entity file: site_domain.rs ```rs //! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1 use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "site_domain")] pub struct Model { #[sea_orm(primary_key, auto_increment = false, column_type = "Text")] pub domain: String, pub site_id: i64, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( belongs_to = "super::site::Entity", from = "Column::SiteId", to = "super::site::Column::SiteId", on_update = "NoAction", on_delete = "NoAction" )] Site, } impl Related for Entity { fn to() -> RelationDef { Relation::Site.def() } } impl ActiveModelBehavior for ActiveModel {} ```

But, if I remove the second foreign key constraint, then it generates the has_many relation for finding the associated Site for a SiteDomain:

CREATE TABLE site (
    site_id BIGSERIAL PRIMARY KEY,
    custom_domain TEXT   -- does NOT have a foreign key relation!
);

CREATE TABLE site_domain (
    domain TEXT PRIMARY KEY,
    site_id BIGINT NOT NULL REFERENCES site(site_id)
);
Entity file: site.rs ```rs //! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1 use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "site")] pub struct Model { #[sea_orm(primary_key)] pub site_id: i64, #[sea_orm(column_type = "Text", nullable)] pub custom_domain: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm(has_many = "super::site_domain::Entity")] SiteDomain, } impl Related for Entity { fn to() -> RelationDef { Relation::SiteDomain.def() } } impl ActiveModelBehavior for ActiveModel {} ```
Entity file: site_domain.rs ```rs //! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1 use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "site_domain")] pub struct Model { #[sea_orm(primary_key, auto_increment = false, column_type = "Text")] pub domain: String, pub site_id: i64, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( belongs_to = "super::site::Entity", from = "Column::SiteId", to = "super::site::Column::SiteId", on_update = "NoAction", on_delete = "NoAction" )] Site, } impl Related for Entity { fn to() -> RelationDef { Relation::Site.def() } } impl ActiveModelBehavior for ActiveModel {} ```

Expected Behavior

There should be multiple cases generated, one of which should be the simple has_many case.

Actual Behavior

Only one Relation::SiteDomain case is generated, which seems to be "overwriting" the has_many case with the belongs_to case.

Perhaps the naming should be numerical, e.g. SiteDomain1, SiteDomain2, or alternatively suffix-based, like SiteDomainCustomDomain for the custom_domain relation because it sees plain SiteDomain (just the has_many) already exists, or having that be named SiteDomainSite or similar.

Reproduces How Often

The above minimal example consistently generates the entity files as described.

Background

This issue was discovered while attempting to do a JOIN between the site and site_domain tables. It was failing to find results, and on inspection, the query that was being generated was incorrect. After debugging, I realized that the wrong JOIN condition was used, per the entity file.

I was attempting to write code that would execute this SQL query:

SELECT *
FROM site
JOIN site_domain
    ON site.site_id = site_domain.site_id
WHERE site_domain.domain = $1

To do this, I wrote the following SeaORM code:

let model = Site::find()
    .join(JoinType::Join, site::Relation::SiteDomain.def())
    .filter(site_domain::Column::Domain.eq(domain))
    .one(txn)
    .await?;

However the Relation::SiteDomain that was generated was not has_many:

#[sea_orm(
    belongs_to = "super::site_domain::Entity",
    from = "Column::CustomDomain",
    to = "super::site_domain::Column::Domain",
    on_update = "NoAction",
    on_delete = "NoAction"
)]
SiteDomain,

I was expecting a simple #[sea_orm(has_many = "super::site_domain::Entity")] like the other Relation cases.

Workarounds

Presently, my workaround for this bug is to manually edit the entity file and add the case to the Relation enum. However, I would like to have sea-orm-cli wholly autogenerate my entity files to reduce maintenance burden.

See https://github.com/scpwiki/wikijump/pull/2084

Versions

Tested on sea-orm-cli versions 1.0.0 and 1.0.1