Point by point.
1.
I have a legacy database for which I have to write a few data-handling utilities. Using the Mapper pattern, without ORM / ActiveRecord style, made things for me about as easy when writing queries as ActiveRecord would. It is operating on nice composable objects that resemble SQL clauses, shielded from SQL injections.
Objects being 'passive' allowed for more flexibility / uniformity: a result of a complex join is a named tuple, as is a result of a simple select. There's no identity to care about, no cached objects with the same identity.
All updates are explicit; not a "save" of some state altered elsewhere, no hooks running on .save()
, etc. This made efficient batch updates trivial, without troubling if the right data are sent to the DB. Both were benefits in my case. In general case, 'it depends'. For instance, I had to manually fetch database-generated IDs after inserts. Running this query explicitly is a bit of extra work. Being able to do that in one query instead of one per record was a huge boon in my case.
SQLAlchemy has a layered design that allows you to access the lower "mapper" level even if you declare things on upper ORM level and normally operate on it. In Django, for instance, it's not as straightforward if/when still possible.
2.
In the example, the 'repository' looks like a level built above the 'mapper'. The repository could have been built on top of plain DBAPI, but the mapper makes a few things simpler, like nicer parameter binding, named tuples for the result sets, and a wrapper above plain SQL with composable, reusable parts.
The mapper also provides a certain degree of database independence. E.g. SQL Server and Postgres have different ways to concatenate strings; the mapper provides a unified interface.
3.
You write your select
where you use it. If you have a select that you constantly reuse in different contexts, you can put it into a method or function. Most of the selects have one use and are built on the spot.
A nice feature of SQLAlchemy's design is that you can easily store conditions and whole where
clauses and reuse them across select / update / delete statements.
4.
Question.query.filter_by(text = text).all()
uses an implicit transaction.
db.session.query(Question).filter(Question.text == text).all()
uses an explicit transaction.
Explicit transactions give you a peace of mind with DML. They are important with select
s, too, when you are querying a quickly changing database and want your several related select
s see the same consistent state.
I usually write a trivial wrapper around sessionmaker
and write things like so:
with my_database.transaction() as trans:
records = trans.query(...)
...
updated = trans.execute(...).rowcount
# Here the transaction commits if all went well.
When I definitely know no DML should run in this block, I use .readonly_transaction()
that always rolls back.
In many cases, the implicit transaction is fine. Django allows you to decorate a method with @transaction.atomic
and have a semi-explicit transaction control, sufficient in 99% of cases. But sometimes you need even finer granularity.