Entity Manager

The optional entity-manager feature adds an explicit persistence context for aggregate workflows.

An EntityManager owns a database handle, caches loaded models by primary key, tracks loaded aggregate-side relations for aggregate saves, and also supports managed lifecycles through persist, merge, remove, detach, and flush.

Enabling the Feature

[dependencies]
tideorm = { version = "0.9.13", features = ["postgres", "entity-manager"] }

Use the backend feature you need (postgres, mysql, or sqlite) alongside entity-manager.

Aggregate Workflow

#![allow(unused)]
fn main() {
use std::sync::Arc;

use tideorm::prelude::*;

#[tideorm::model(table = "users")]
struct User {
    #[tideorm(primary_key, auto_increment)]
    id: i64,
    name: String,

    #[tideorm(has_many = "Post", foreign_key = "user_id")]
    posts: HasMany<Post>,
}

#[tideorm::model(table = "posts")]
struct Post {
    #[tideorm(primary_key, auto_increment)]
    id: i64,
    user_id: i64,
    title: String,
}

async fn aggregate_example(db: Arc<Database>) -> tideorm::Result<()> {
    let entity_manager = EntityManager::new(db);

    let mut user = entity_manager
        .find::<User>(1)
        .await?
        .expect("user should exist");

    entity_manager.load(&mut user.posts).await?;

    user.posts
        .as_mut()
        .expect("posts should be loaded")
        .push(Post {
            id: 0,
            user_id: 0,
            title: "Draft".to_string(),
        });

    let user = entity_manager.save(&user).await?;

    assert!(user.posts.get_cached().is_some());
    Ok(())
}
}

Use this facade when you want to load a root aggregate, explicitly load its relations, mutate the graph, then persist the loaded graph back through the same context.

Managed Lifecycle Workflow

#![allow(unused)]
fn main() {
async fn managed_example(db: Arc<Database>) -> tideorm::Result<()> {
    let entity_manager = EntityManager::new(db);

    let user = entity_manager
        .find_managed::<User>(1)
        .await?
        .expect("user should exist");

    user.edit(|user| user.name = "Updated".to_string());

    entity_manager.flush().await?;

    let inserted = entity_manager.persist(User {
        id: 0,
        name: "New User".to_string(),
        posts: Default::default(),
    });
    entity_manager.flush().await?;
    assert!(inserted.get().id > 0);

    entity_manager.remove(&user);
    entity_manager.flush().await?;

    Ok(())
}
}

Managed entities expose:

  • managed.get() to clone the current managed value.
  • managed.edit(...) to mutate the managed value in place.
  • managed.replace(...) to replace the current managed value wholesale.
  • managed.state() to inspect whether the entity is New, Managed, Removed, or Detached.

merge(...) attaches a detached instance into the current context. detach(...) keeps the in-memory value but removes it from future flushes. clear() detaches the whole context.

Compatibility Helpers

The EntityManager facade is the recommended API, but the generated compatibility entry points remain available:

  • Model::find_in_entity_manager(pk, &entity_manager)
  • relation.load_in_entity_manager(&entity_manager)
  • save_with_entity_manager(&model, &entity_manager)

The explicit tracked-collection helper also remains available when needed:

#![allow(unused)]
fn main() {
tideorm::entity_manager::TrackedHasManyEntityManagerExt::load(&mut user.posts, &entity_manager)
    .await?;
}

When a model itself was loaded through EntityManager::find(...) or find_in_entity_manager(...), plain relation read helpers such as load(), load_with(...), count(), and exists() continue to query through that same entity-manager database handle even if no global database is configured.

Use entity_manager.load(&mut relation) or relation.load_in_entity_manager(&entity_manager) when the relation should become tracked for aggregate synchronization on save() or flush().

What Aggregate Saves Synchronize

  • Root saves use the entity manager's database handle.
  • Loaded HasOne<T>, HasMany<T>, and HasManyThrough<T, P> relations are synchronized with the saved aggregate.
  • New related models are inserted, changed related models are updated, removed HasOne<T> or HasMany<T> children are deleted by the child model's own primary key, and HasManyThrough<T, P> pivot rows are attached or detached as needed.
  • Both foreign_key and local_key are honored, so non-id relation keys work.
  • Nested loaded child graphs continue through the same context.
  • Unloaded relations remain untouched.

Primary Key Support

Entity-manager identity tracking works with the same primary-key shapes as the generated model APIs:

  • Auto-increment numeric keys, such as User::find_in_entity_manager(1, &entity_manager).
  • Natural keys, such as ApiKey::find_in_entity_manager("api-key-1".to_string(), &entity_manager).
  • Composite keys, such as Membership::find_in_entity_manager((team_id, member_id), &entity_manager).

Tracked HasOne<T> and HasMany<T> synchronization uses the related model's actual primary key for updates and deletes, so natural-key and composite-key children work the same way as numeric-key children.

Notes

  • EntityManager is explicit. It does not replace TideORM's global database APIs.
  • EntityManager::save() and save_with_entity_manager() are designed for aggregate workflows around loaded models plus loaded HasOne<T>, HasMany<T>, and HasManyThrough<T, P> relations.
  • BelongsTo<T> participates in entity-manager-aware loads and identity reuse, but aggregate saves do not cascade BelongsTo<T> updates.
  • Reuse the same EntityManager for aggregate loads, managed edits, and flushes when you want one consistent persistence context.