#3183·sea-orm

Nested ActiveModelEx::save() fails to insert a new has_many child with a composite, non-auto-increment primary key

Author: ziimakcCreated Aug 22, 2026Updated Sep 10, 2026

Description

Nested ActiveModelEx::save() (SeaORM 2.0) picks insert-vs-update per related row via is_update(), which just checks whether the primary key columns are Set/Unchanged. For an entity with a composite, non-auto-increment primary key (e.g. a join/ownership table keyed by (parent_id, tag)), the key is always fully set when building a new row, so a brand-new child is misdiagnosed as an update. The resulting UPDATE matches zero rows and save() fails with RecordNotFound, instead of inserting the row.

This works fine for auto-increment primary keys (a fresh row naturally has NotSet on the pk), but composite non-auto-increment keys are a common shape for has_many children (join tables, per-user records, etc.), so save() can't be used to add a new child to an already-persisted parent in that case. The same misdiagnosis also hits any entity whose (single-column) primary key is a UUID assigned application-side rather than by the database — a very common pattern — since that pk is likewise always Set on a fresh row, not NotSet.

Steps to reproduce

rust
use sea_orm::entity::prelude::*;
use sea_orm::{ConnectionTrait, Database, Schema};

mod parent {
    use sea_orm::entity::prelude::*;

    #[sea_orm::model]
    #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
    #[sea_orm(table_name = "parent")]
    pub struct Model {
        #[sea_orm(primary_key, auto_increment = false)]
        pub id: i32,
        #[sea_orm(has_many)]
        pub children: HasMany<super::child::Entity>,
    }

    impl ActiveModelBehavior for ActiveModel {}
}

mod child {
    use sea_orm::entity::prelude::*;

    // Composite, non-auto-increment primary key.
    #[sea_orm::model]
    #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
    #[sea_orm(table_name = "child")]
    pub struct Model {
        #[sea_orm(primary_key, auto_increment = false)]
        pub parent_id: i32,
        #[sea_orm(primary_key, auto_increment = false)]
        pub tag: i32,
        #[sea_orm(belongs_to, from = "parent_id", to = "id")]
        pub parent: HasOne<super::parent::Entity>,
    }

    impl ActiveModelBehavior for ActiveModel {}
}

#[tokio::main]
async fn main() -> Result<(), DbErr> {
    let db = Database::connect("sqlite::memory:").await?;
    let builder = db.get_database_backend();
    let schema = Schema::new(builder);
    db.execute(&schema.create_table_from_entity(parent::Entity)).await?;
    db.execute(&schema.create_table_from_entity(child::Entity)).await?;

    parent::ActiveModel::builder().set_id(1).insert(&db).await?;

    let loaded = parent::Entity::load()
        .filter(parent::COLUMN.id.eq(1))
        .one(&db)
        .await?
        .expect("parent exists");

    let mut active: parent::ActiveModelEx = loaded.into();
    active.children.push(child::ActiveModel::builder().set_tag(42));

    let result = active.save(&db).await; // <- fails
    println!("{:?}", result.map(|_| ()));

    Ok(())
}

Cargo.toml dependency: sea-orm = { version = "=2.0.2", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros"] }

Expected behaviour

The new child row is INSERTed.

Actual behaviour

Err(RecordNotFound Error: Failed to find updated item)

No row is inserted; save() returns an error instead.

Versions

  • sea-orm: 2.0.2
  • rustc: latest stable

Happy to help narrow this down further if useful — thanks for the 2.0 nested-save feature, it's great for the auto-increment case!