Migrating from PonyORM¶
NextORM is heavily inspired by PonyORM — the query DSL, session model, and relation system will feel familiar. This page covers what changed and what to watch out for.
Overview¶
The biggest conceptual shift is that entities are decoupled from the database
instance. In PonyORM every entity inherits from db.Entity; in NextORM you
subclass Entity directly and the database discovers entities automatically (or
you scope them explicitly).
Everything else — db_session, commit(), rollback(), flush(),
the generator-expression query syntax, lifecycle hooks — works the same way.
Entity definition¶
The most visible change is field declaration syntax. PonyORM uses descriptor calls; NextORM uses type annotations:
# PonyORM
class Product(db.Entity):
name = Required(str)
price = Optional(float)
id = PrimaryKey(int, auto=True)
# NextORM
class Product(Entity):
name: Req[str]
price: Opt[float]
id: PK[int] # auto-increment is the default for PK[int]
The table name defaults to the class name lower-cased (same as PonyORM).
Override it with _table_name_ = "my_table".
Relations¶
PonyORM infers relationship types from the combination of attributes on both
sides. NextORM requires an explicit Single[T] (FK side) and Set[T]
(collection side):
# PonyORM
class Order(db.Entity):
lines = Set('OrderLine')
class OrderLine(db.Entity):
order = Required(Order) # PonyORM infers the FK from Required(...)
# NextORM
class Order(Entity):
lines: Set["OrderLine"] # back-reference — see note below
class OrderLine(Entity):
order: Single[Order] # explicit FK
Single[T] creates a NOT NULL FK with ON DELETE CASCADE.
Single[T | None] creates a nullable FK with ON DELETE SET NULL.
Note
Back-references are optional.
You can omit lines: Set["OrderLine"] on Order entirely — the FK
column on OrderLine still exists and works. The only thing you lose is
collection access from the Order side (order.lines). Add the
Set[T] attribute whenever you need that reverse navigation.
When using validate_relations=True in generate_mapping(),
every Set[T] you do declare must have a matching Single[T] back-reference
on the target, and ambiguous cases require RelationSpec(reverse="...").
Querying¶
The generator syntax is identical:
# Both PonyORM and NextORM
from nextorm.generators import select
results = select(p for p in Product if p.price > 100).fetch_all()
The main difference is the terminal call. PonyORM lets you iterate a query
or slice it (q[:]). NextORM always requires an explicit terminal:
# PonyORM
results = list(select(p for p in Product))
first = select(p for p in Product).first()
# NextORM
results = select(p for p in Product).fetch_all()
first = select(p for p in Product).first()
The fluent QuerySet API is also available, including a class-level shortcut
that locates the database automatically:
# via db reference
db.select(Product).filter(Product.price > 100).order_by(Product.price).fetch_all()
# class-level shortcut (no explicit db needed)
Product.select().filter(Product.price > 100).fetch_all()
# async equivalents
await db.aselect(Product).filter(Product.price > 100).fetch_all()
await Product.aselect().filter(Product.price > 100).fetch_all()
# classic Entity.get() style
user = User.get(email="alice@example.com") # sync
user = await User.aget(email="alice@example.com") # async
Primary key lookup¶
# PonyORM
p = Product[42]
# NextORM — same syntax; raises KeyError when not found
p = Product[42]
# Composite PK — pass individual values (Python tuple syntax)
line = OrderLine[order_id, product_id]
Saving and deleting¶
NextORM tracks new instances automatically — the same as PonyORM.
Creating an entity inside a db_session schedules it for INSERT; the
session commits the change on exit:
# Both PonyORM and NextORM work the same way
with db_session:
p = Product(name="Widget", price=9.99)
# ← INSERT fires automatically on commit
When you need the auto-generated PK before the session ends, call
flush() explicitly:
with db_session:
p = Product(name="Widget", price=9.99)
flush() # INSERT → p.id is now available
print(p.id)
save() and
insert() are also available when you want
fine-grained control or need to insert outside a session context.
Deletion is the same in both ORMs:
p.delete() # shortcut (entity must have been loaded via a session)
db.delete_instance(p) # explicit alternative
N+1 and eager loading¶
PonyORM automatically batches FK look-ups into a single WHERE pk IN (...)
query when it detects you are iterating a result set. NextORM does not do
this automatically — you must declare relations to prefetch:
# Without prefetch — one extra query per post (N+1)
posts = db.select(Post).fetch_all()
for post in posts:
print(post.author.name) # lazy SELECT each time
# With prefetch — one batch query for all authors
posts = db.select(Post).prefetch(Post.author).fetch_all()
for post in posts:
print(post.author.name) # pre-populated, no extra SQL
What is not yet implemented¶
A few PonyORM features are not yet available in NextORM:
Transparent lazy-load batching (PonyORM’s N+1 coalescing) — use
prefetch()explicitly.@db_session(retry=n)— session-level automatic retry on deadlock.Entity.select(lambda)— passing a lambda directly toEntity.select()is not supported; useEntity.select().where(lambda)or the generator-expression syntaxselect(x for x in Entity if ...)instead.Hybrid properties (usable inside both Python and generator queries).
Oracle and CockroachDB providers — SQLite, PostgreSQL, and MariaDB are fully supported.