Skip to content

Session

Creating a session factory

from mongotic import create_engine
from mongotic.orm import sessionmaker

engine  = create_engine("mongodb://localhost:27017")
Session = sessionmaker(bind=engine)

sessionmaker returns a class. Call it to open a session:

session = Session()

with Session() as session:
    session.add(User(name="Alice", email="alice@example.com"))
    session.commit()
# session.close() is called automatically on exit

On exit, close() discards any un-flushed staged changes.


Writing

add(instance) and add_all(instances)

Stage one or more new documents for insertion. The document is not written to MongoDB until you call flush() or commit().

session.add(User(name="Alice", email="alice@example.com"))
session.add_all([
    User(name="Bob",   email="bob@example.com"),
    User(name="Carol", email="carol@example.com"),
])
session.commit()

delete(instance)

Stage an existing document for deletion. Requires the instance to have a valid _id.

user = session.scalars(select(User).where(User.email == "bob@example.com")).first()
session.delete(user)
session.commit()

Field assignment (auto-tracking)

Assigning a field on an instance that is already attached to a session automatically stages an update:

user = session.scalars(select(User).where(User.email == "alice@example.com")).first()
user.company = "Acme"   # staged automatically
session.commit()        # writes the $set to MongoDB

flush() vs commit()

flush() commit()
What it does Writes all staged ops to MongoDB immediately Alias for flush()
After the call _id is available on inserted instances Same
Can be undone? No — changes are persisted No
with Session() as session:
    new_user = User(name="Dave", email="dave@example.com")
    session.add(new_user)
    session.flush()
    print(new_user._id)   # ObjectId string is now available
    session.commit()      # no-op if nothing else was staged

No multi-document transactions

mongotic does not wrap writes in MongoDB transactions. Each document write is individually atomic, but cross-document atomicity is not guaranteed. See Design for the rationale.


rollback()

Discards staged (not yet flushed) changes. It cannot undo writes already sent to MongoDB.

session.add(User(name="Temp"))
session.rollback()   # "Temp" user is never written

close()

Discards un-flushed staged changes and is called automatically by the context manager. Equivalent to rollback() followed by releasing the session.


refresh(instance)

Reloads all fields of an instance from the database in-place. Useful after an external process has modified the document, or to confirm the current DB state.

with Session() as session:
    user = session.scalars(select(User).where(User.name == "Alice")).one()
    print(user.age)  # 25

    # some external process updates the document...

    session.refresh(user)
    print(user.age)  # 30 — reloaded from DB
  • Raises ValueError if the instance has no _id (was never persisted).
  • Raises NotFound if the document no longer exists in the database.
  • Clears any pending field-level updates for the instance after refresh.

merge(instance)

Stages an instance for an upsert on the next flush() / commit(). If _id is already set, the existing document is replaced; if not, a new document is inserted.

with Session() as session:
    # If a user with this _id exists → update; otherwise → insert
    user = User(name="Alice", age=30)
    user._id = "507f1f77bcf86cd799439011"   # _id is a private attr, set after construction
    merged = session.merge(user)
    session.flush()

    print(merged._id)  # "507f1f77bcf86cd799439011"
Scenario Behaviour
instance._id is set and document exists Replaces document on flush
instance._id is set but document doesn't exist Inserts document on flush
instance._id is None Behaves like session.add()

Note

merge() uses MongoDB's replace_one(..., upsert=True) under the hood. Any pending field-level updates for the same instance are discarded — the full in-memory state is written on flush.


expunge(instance)

Detaches an instance from the session. Any pending field-level updates for that instance are discarded.

user = session.scalars(select(User).where(User.name == "Alice")).one()
user.age = 99           # staged as dirty
session.expunge(user)   # removed from dirty tracking
assert session.dirty == []
assert user._session is None

After expunging, the instance may be freely modified or re-added to another session:

session.add(user)   # re-attaches; staged in session.new

expunge() is idempotent — calling it twice on the same instance does not raise.


expire(instance)

Marks an instance as stale and clears any pending field updates for it. The in-memory attribute values are retained, but the _expired flag signals that they may not match the database.

user = session.scalars(select(User).where(User.name == "Alice")).one()
user.age = 99
session.expire(user)
# user._expired is True; user.age is still 99 in memory
session.refresh(user)   # explicit reload from DB
print(user.age)         # DB value

No lazy reload

expire() does not trigger a database round-trip on the next attribute access. Call session.refresh(user) explicitly when you need the current DB state.

This is intentional: mongotic supports both sync and async sessions. Lazy attribute fetching would require await in the async path, which is incompatible with Python attribute access syntax. Explicit refresh() is the uniform API in both modes.


Session state properties

Inspect the session's pending state before flushing:

with Session() as session:
    user = User(name="Alice", age=25)
    session.add(user)

    print(session.new)      # [User(name="Alice", ...)]
    print(session.dirty)    # []
    print(session.deleted)  # []

    session.flush()

    user.age = 26
    print(session.dirty)    # [User(name="Alice", age=26)]

    session.delete(user)
    print(session.deleted)  # [User(name="Alice", ...)]
Property Returns Description
session.new list[Model] Instances staged for insertion
session.dirty list[Model] Instances with pending field changes
session.deleted list[Model] Instances staged for deletion

All three properties return shallow copies — mutating the returned list does not affect session state. All are empty after flush().


Async sessions

The sync Session API has a full async counterpart in mongotic.asyncio. Methods that perform I/O become awaitable; methods that only mutate in-memory state (add, delete, rollback, expunge, expire, merge) remain synchronous.

See Async for setup, usage examples, and a sync/async cheat sheet.