Ok, so this is what I have for today. It works, and handles all kinds of
corner cases and yet it's not quite what I want. It does everything as a
joinedload. It's much easier to use now though.
You can do things like:
q = Session.query(Animal)
for animal in yielded_load(q, (joinedload(Animal.owner).joinedload(Human.
family),
joinedload(Animal.species).joinedload(Species
.phylum)):
do_something(animal)
It says joinedload() but it doesn't actually pay attention to that, it just
uses it to determine the path. It would be really nice to be able to
specify that some things should be fetched using subqueryload(), but that
would require unpacking/manipulating the Load objects and I don't think
there's a supported interface for that. Additionally, it would be nice if
could notice that paths share a common prefix and only fetch those once.
Still, for the amount of code it's pretty effective.
from itertools import groupby, islice
from sqlalchemy.orm import attributes, Load, aliased
from sqlalchemy import tuple_
def yielded_load(query, load_options, N=1000):
# Note: query must return only a single object (for now anyway)
main_query = query.yield_per(N)
main_res = iter(main_query)
while True:
# Fetch block of results from query
objs = list(islice(main_res, N))
if not objs:
break
for load_option in load_options:
# Get path of attributes to follow
path = load_option.path
pk = path[0].prop.parent.primary_key
# Generate query that joins against original table
child_q = main_query.session.query().order_by(*pk)
for i, attr in enumerate(path):
if i == 0:
# For the first relationship we add the target and the
pkey columns
# Note: add_columns() doesn't work here?
with_entities() does....
next_table = attr.prop.target
child_q = child_q.join(next_table, attr)
child_q = child_q.with_entities(attr.prop.mapper).
add_columns(*pk)
if attr.prop.order_by:
child_q = child_q.order_by(*attr.prop.order_by)
opts = Load(attr.prop.mapper)
else:
# For all relationships after the first we can use
contains_eager
# Note: The aliasing is to handle cases where the
relationships loop
next_table = aliased(attr.prop.target)
child_q = child_q.join(next_table, attr, isouter=True)
opts = opts.contains_eager(attr, alias=next_table)
child_q = child_q.options(opts)
keys = [[getattr(obj, col.key) for col in pk] for obj in objs]
child_q = child_q.filter(tuple_(*pk).in_(keys))
# Here we use the fact that the first column is the target
object
collections = dict((k, [r[0] for r in v]) for k, v in groupby(
child_q,
lambda x: tuple([getattr(x, c.key) for c in pk])
))
for obj in objs:
# We can traverse many-to-one and one-to-many
if path[0].prop.uselist:
attributes.set_committed_value(
obj,
path[0].key,
collections.get(
tuple(getattr(obj, c.key) for c in pk),
())
)
else:
attributes.set_committed_value(
obj,
path[0].key,
collections.get(
tuple(getattr(obj, c.key) for c in pk),
[None])[0]
)
for obj in objs:
yield obj
--
Martijn
--
You received this message because you are subscribed to the Google Groups
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.