Skip to content

class Athena::ORM::EntityManager
inherits Reference #

The central access point to the ORM, used to persist, find, and remove entities.

em = AORM::EntityManager.new connection

user = User.new
user.name = "George"

em.persist user
em.flush

user.id # => 1

em.find(User, 1).same? user # => true

An entity manager tracks the entities it loads or is given in a unit of work, and isn't safe to share between fibers. Use an AORM::EntityManagerFactory to create one for each unit of work, such as a request or a job.

Writing Changes#

Only #flush writes to the database. Methods such as #persist and #remove only schedule an entity to be inserted or deleted, and changes to the entities' properties are detected when flushing. #flush then writes all of the pending changes in a single transaction, ordering the statements so that the rows referenced by foreign keys exist before the rows referencing them. Don't make assumptions about the number or order of the statements a flush executes.

There's no need to tell the entity manager about changes to an entity it manages:

user = em.find! User, 1
user.name = "Jim"

em.flush # => UPDATE users SET name = ? WHERE id = ?

Prefer flushing once a unit of work is done over flushing after every change, since each flush computes the changes of every managed entity.

Identity Map#

An entity manager holds at most one instance of each entity, keyed by its identifier. Finding an entity that's already been loaded returns the same instance without querying the database, whichever way it's loaded:

user = em.find! User, 1
user.name = "Jim"

em.repository(User).find_one_by(name: "Jim").same? user # => true

Since a loaded entity isn't overwritten by later queries, unflushed changes are kept. Use #refresh to reload an entity, or #clear to start over with an empty identity map.

Entity States#

An entity is in one of the states of AORM::UnitOfWork::EntityState in relation to an entity manager:

  • New - It has no persistent identity, and isn't associated with the entity manager yet, such as an entity that was just instantiated.
  • Managed - It's associated with the entity manager, which writes its changes to the database when flushing.
  • Removed - It's associated with the entity manager, and will be deleted when flushing.
  • Detached - It has a persistent identity, but isn't associated with the entity manager anymore, such as after #clear.

The effects of #persist, #remove, and #detach depend on the entity's state, as described on each method.

Transactions#

Each #flush runs in its own transaction, so a failed flush leaves the database unchanged. When a flush fails, the entity manager is also closed, and should be discarded along with its entities, whose state no longer matches the database.

Wrap more work in one transaction via #wrap_in_transaction, or with #begin_transaction, #commit, and #rollback:

em.wrap_in_transaction do
  user = em.find! User, 1
  user.balance -= 100

  em.connection.exec "INSERT INTO audit_log (message) VALUES (?)", "Withdrew 100"
end

Transactions nest; a transaction started while another is active is a savepoint within it. A flush in an explicit transaction therefore only rolls back its own savepoint if it fails.

Included modules

Athena::ORM::EntityManagerInterface

Constructors#

.new(connection : DB::Connection, event_dispatcher : ACTR::EventDispatcher::Interface | Nil = nil, *, metadata_cache : AORM::Mapping::MetadataCache | Nil = nil)#

Creates an entity manager executing its queries on connection.

event_dispatcher receives the Events::PreFlushEventArgs, Events::OnFlushEventArgs, Events::PostFlushEventArgs and Events::OnClearEventArgs events. Per-entity events, such as Events::PrePersistEventArgs, are only delivered to the entity's lifecycle callbacks.

metadata_cache shares class metadata with other entity managers on the same database; without one, metadata is built for this entity manager alone.

View source

Methods#

#begin_transaction : Nil#

Starts a transaction, or a savepoint if one is already active. It must be ended with #commit or #rollback.

em.begin_transaction

begin
  # ...

  em.flush
  em.commit
rescue ex
  em.rollback
  em.close

  raise ex
end

Tip

AORM::EntityManager#wrap_in_transaction takes care of flushing, committing, and handling errors.

View source

#class_metadata(for entity_class : AORM::Entity.class) : AORM::Mapping::ClassInterface#

Returns the mapping metadata of entity_class.

View source

#clear : Nil#

Detaches every entity this entity manager manages, discarding any changes that weren't flushed. Afterwards, entities are loaded from the database again rather than from the identity map.

View source

#close : Nil#

Clears the entity manager and marks it closed.

A closed entity manager raises on #persist, #remove, #refresh, and #flush.

The connection, and any transaction open on it, are left to whoever provided the connection.

View source

#closed? : Bool#

Returns true if this entity manager was closed via #close, or by a failed flush.

View source

#commit : Nil#

Commits the innermost transaction, or releases its savepoint if it's nested.

The entity manager isn't flushed first; call #flush before committing.

View source

#connection : AORM::Connection#

Returns the connection this entity manager executes its queries on.

View source

#contains(entity : AORM::Entity) : Bool#

Returns true if entity is managed by this entity manager, i.e. it was loaded or persisted by it, and isn't removed.

View source

#create_native_query(sql : String, rsm : Query::ResultSetMapping) : NativeQuery#

Creates a query executing the native sql, whose results are hydrated as described by rsm.

rsm = AORM::Query::ResultSetMapping.new
rsm.add_entity_result User, "u"
rsm.add_field_result "u", "id", "id"
rsm.add_field_result "u", "name", "name"

query = em.create_native_query "SELECT u.id, u.name FROM users u WHERE u.name LIKE ?", rsm
query.set_parameter 1, "G%"

query.get_result # => [#<User:0x7f3a1c2b5e40 @id=1, @name="George">]

See AORM::NativeQuery for more information.

View source

#detach(entity : AORM::Entity) : Nil#

Detaches the managed entity from this entity manager, which then no longer writes its changes to the database or returns it from the identity map. Pending changes that weren't flushed are discarded, including a scheduled insert or deletion.

Entities that aren't managed are ignored. The operation is also applied to the associated entities of associations that cascade "detach". Other entities that reference entity keep referencing it.

View source

#event_dispatcher : ACTR::EventDispatcher::Interface | ::Nil#

Returns the event dispatcher the flush and clear events are dispatched to, if any.

View source

#find(entity_class : T.class, id : Hash(String, Int | String) | Int | String, lock_mode : AORM::LockMode = :none, lock_version : Int32 | Nil = nil) : AORM::Entity | Nil forall T#

Returns the entity of type entity_class with the identifier id, or nil if there isn't one.

em.find User, 1 # => #<User:0x7f3a1c2b5e40 @id=1, @name="George">
em.find User, 2 # => nil

An entity in the identity map is returned without querying the database. Entities with a composite identifier are found with a Hash holding a value for each identifier field, see Composite Keys. Raises an AORM::Exceptions::MissingIdentifierField if the hash is missing a field, or if a single value is given for a composite identifier.

Todo

Row locking isn't supported yet, see AORM::LockMode.

View source

#find!(entity_class : T.class, id : Hash(String, Int | String) | Int | String, lock_mode : AORM::LockMode = :none, lock_version : Int32 | Nil = nil) : AORM::Entity forall T#

Returns the entity of type entity_class with the identifier id. Raises an AORM::Exceptions::NoResult if there isn't one.

See #find.

View source

#flush : Nil#

Writes all of the changes to the entities this entity manager manages to the database, in a single transaction.

This inserts persisted entities, updates changed managed entities, deletes removed entities, and writes the changes to their associations. Changes are only detected on the owning side of an association, see the associations section of the manual.

Raises an exception if a new entity is found through an association that doesn't cascade "persist". If the flush fails, its transaction is rolled back and the entity manager is closed.

View source

#persist(entity : AORM::Entity) : Nil#

Makes the new entity managed, so that it's inserted on the next #flush.

user = User.new
user.name = "George"

em.persist user
em.flush # => INSERT INTO users (name) VALUES (?)

Depending on entity's state:

  • New - It becomes managed, and its AORMA::PrePersist callbacks run.
  • Managed - Nothing changes, but associated entities are still persisted if the association cascades "persist".
  • Removed - It's managed again, and won't be deleted.

The operation is also applied to the associated entities of associations that cascade "persist", see the associations section of the manual.

Database generated identifiers are assigned when the entity is inserted, so they aren't available until the flush.

Warning

Don't pass detached entities. Any entity that isn't known to the entity manager is treated as new, so persisting a detached entity tries to insert it again.

View source

#refresh(entity : AORM::Entity, lock_mode : AORM::LockMode = :none) : Nil#

Reloads the managed entity's columns from the database, discarding any changes that haven't been flushed. Raises an exception if entity isn't managed.

user = em.find! User, 1
user.name = "Not saved"

em.refresh user
user.name # => "George"

Todo

Associations aren't refreshed, even if the association cascades "refresh". Row locking isn't supported yet either, see AORM::LockMode.

View source

#remove(entity : AORM::Entity) : Nil#

Schedules the managed entity to be deleted on the next #flush.

user = em.find! User, 1

em.remove user
em.flush # => DELETE FROM users WHERE id = ?

Depending on entity's state:

  • New or Removed - Nothing changes, but associated entities are still removed if the association cascades "remove".
  • Managed - It becomes removed, and its AORMA::PreRemove callbacks run.
  • Detached - Raises an exception.

The operation is also applied to the associated entities of associations that cascade "remove", see the associations section of the manual.

Until it's flushed, a removed entity can still be found by queries and stays in the collections that contain it. Once it's deleted, a database generated identifier is set back to nil, while the rest of its state stays as it was.

View source

#repository(entity_class : AORM::Entity.class) : AORM::RepositoryInterface#

Returns the repository of entity_class.

em.repository(User).find_by name: "George" # => [#<User:0x7f3a1c2b5e40 @id=1, @name="George">]

The repository is an AORM::EntityRepository of the entity, or the entity's custom repository class if it has one, see Custom Repositories.

For every concrete entity, an overload returns its repository typed as AORM::EntityRepository of the entity, or as the entity's custom repository class. This overload is used for a class only known as an AORM::Entity.class.

View source

#rollback : Nil#

Rolls back the innermost transaction, or to its savepoint if it's nested.

Entities keep the state they had in memory, which may no longer match the database. Close the entity manager via #close and discard it along with its entities.

View source

#unit_of_work : AORM::UnitOfWork#

Returns the unit of work tracking this entity manager's entities.

View source

#wrap_in_transaction : T#

Runs the block in a transaction, flushing before it commits, and returns the block's value. If the block or the flush raises, the entity manager is closed and the transaction rolled back.

user = em.wrap_in_transaction do
  user = User.new
  user.name = "George"

  em.persist user

  user
end

user.id # => 1
View source