On 7/31/15 2:51 PM, Douglas Russell wrote:
I have a structure like this:
A
└── B
└── C
`A` objects can have many `B` objects connected to them and a `B`
objects can be connected to many `A` objects. The same goes for `B`
and `C`. As they are all Many-to-Many, Association tables are used.
It is useful to use SQLAlchemy relationships to be able get a list of
`B` objects connected to a `C` for example. That is easy and
accomplished in the below code.
It is also useful to be able to do the same thing for indirect
membership between `A` and `C`. I.e. the list of `A` objects which are
the parents of `B` objects that are the parents of `C` objects. I can
also accomplish this with the more complicated relationship
a_list = relationship(
"A",
secondary="join(BCAssociation, B, BCAssociation.b_id ==
B.id).join(ABAssociation, ABAssociation.b_id == B.id)",
primaryjoin="C.id == BCAssociation.c_id",
secondaryjoin="A.id == ABAssociation.a_id",
viewonly=True, backref=backref('c_list')
)
That works perfectly. The code seems a little too complex to post in
an email so I have put it
here: https://gist.github.com/dpwrussell/02519dc06061fab0d89e
The result and query that is run when using that relationship is:
2015-07-31 14:27:41,631 INFO sqlalchemy.engine.base.Engine SELECT
a.id AS a_id, a.name AS a_name
FROM a, b_c_association JOIN b ON b_c_association.b_id = b.id JOIN
a_b_association ON a_b_association.b_id = b.id
WHERE ? = b_c_association.c_id AND a.id = a_b_association.a_id
2015-07-31 14:27:41,631 INFO sqlalchemy.engine.base.Engine (1,)
a1
The problem is if `A`, `B` and `C` are actually all extensions of the
same superclass. The output will be this:
2015-07-31 14:26:21,211 INFO sqlalchemy.engine.base.Engine SELECT
anon_1.object_type AS object_type, a.id AS a_id, anon_1.object_id AS
object_id, anon_1.object_name AS object_name
FROM object JOIN a ON object.id = a.id, b_c_association JOIN
(SELECT object.type AS object_type, object.id AS object_id,
object.name AS object_name, b.id AS b_id
FROM object JOIN b ON object.id = b.id) AS anon_1 ON
b_c_association.b_id = anon_1.b_id JOIN a_b_association ON
a_b_association.b_id = anon_1.b_id
WHERE ? = b_c_association.c_id AND a.id = a_b_association.a_id
2015-07-31 14:26:21,212 INFO sqlalchemy.engine.base.Engine (3,)
b1
OK well first off when diagnosing these subquery-heavy mappings, the
first thing I strongly suggest is to not do your initial dev/testing
against SQLite. There is an extra layer of query rewriting in SQLite to
work around the fact that SQLite cannot accommodate right-nested joins,
and that's why you see that JOIN (SELECT ...) thing happening.
Running it on any other database, I usually default to Postgresql
because it has the most solid and consistent query parser, the
still-incorrect query is a little easier to read and it correctly
reports the issue as that the "object" table is mentioned twice:
sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) table name
"object" specified more than once
[SQL: 'SELECT object.type AS object_type, a.id AS a_id, object.id AS
object_id, object.name AS object_name \nFROM object JOIN a ON object.id
= a.id, b_c_association JOIN (object JOIN b ON object.id = b.id) ON
b_c_association.b_id = b.id JOIN a_b_association ON a_b_association.b_id
= b.id \nWHERE %(param_1)s = b_c_association.c_id AND a.id =
a_b_association.a_id'] [parameters: {'param_1': 3}]
The SQLite version clearly is getting confused because SQLAlchemy's
query rewriting it tripping over that double object table thing but not
raising an error.
So we need to use aliasing to get that secondary table inside of a
sub-namespace. There are probably slicker ways to work it out with
declarative but just to make it work you can build up the join + alias
and the relationship after the fact:
secondary = join(BCAssociation, B, BCAssociation.b_id == B.id).\
join(ABAssociation, ABAssociation.b_id == B.id).alias(None)
C.a_list = relationship(
"A",
secondary=secondary,
primaryjoin=C.id == secondary.corresponding_column(BCAssociation.c_id),
secondaryjoin=A.id ==
secondary.corresponding_column(ABAssociation.a_id),
viewonly=True,
backref=backref('c_list')
)
then on PG we get:
SELECT object.type AS object_type, a.id AS a_id, object.id AS object_id,
object.name AS object_name
FROM (SELECT b_c_association.b_id AS b_c_association_b_id,
b_c_association.c_id AS b_c_association_c_id, object.type AS
object_type, object.id AS object_id, object.name AS object_name, b.id AS
b_id, a_b_association.a_id AS a_b_association_a_id, a_b_association.b_id
AS a_b_association_b_id
FROM b_c_association JOIN (object JOIN b ON object.id = b.id) ON
b_c_association.b_id = b.id JOIN a_b_association ON a_b_association.b_id
= b.id) AS anon_1, object JOIN a ON object.id = a.id
WHERE %(param_1)s = anon_1.b_c_association_c_id AND a.id =
anon_1.a_b_association_a_id
2015-07-31 15:19:57,066 INFO sqlalchemy.engine.base.Engine {'param_1': 3}
a1
you can see the right-nested join in there that SQLite can't do, so on
SQLite the translation gives us:
SELECT object.type AS object_type, a.id AS a_id, object.id AS object_id,
object.name AS object_name
FROM (SELECT b_c_association.b_id AS b_c_association_b_id,
b_c_association.c_id AS b_c_association_c_id, anon_2.object_type AS
object_type, anon_2.object_id AS object_id, anon_2.object_name AS
object_name, anon_2.b_id AS b_id, a_b_association.a_id AS
a_b_association_a_id, a_b_association.b_id AS a_b_association_b_id
FROM b_c_association JOIN (SELECT object.type AS object_type, object.id
AS object_id, object.name AS object_name, b.id AS b_id
FROM object JOIN b ON object.id = b.id) AS anon_2 ON
b_c_association.b_id = anon_2.b_id JOIN a_b_association ON
a_b_association.b_id = anon_2.b_id) AS anon_1, object JOIN a ON
object.id = a.id
WHERE ? = anon_1.b_c_association_c_id AND a.id = anon_1.a_b_association_a_id
2015-07-31 15:21:14,577 INFO sqlalchemy.engine.base.Engine (3,)
a1
you can see its the same thing but the right-nested JOIN is blown up
into a new SELECT (very difficult logic to make that work in most cases).
It selects the `B` object, not the `A` object I was expecting. The
full code for that is also in the GitHub Gist. Just comment-out the
'Working Example' and comment-in the 'Failing Example'.
I'm not sure if this is something that I should have mitigated in my
definition of the relationship (I can't figure out how) or if this is
a limitation of SQLAlchemy.
Also, at this time I think it's worth saying that SQLAlchemy is making
the bulk of what I wish to achieve very easy and because of that, I am
getting to these comparatively complicated requirements that were far
down my TODO list very quickly. So, thanks for that!
Thanks,
Douglas
--
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]
<mailto:[email protected]>.
To post to this group, send email to [email protected]
<mailto:[email protected]>.
Visit this group at http://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.
--
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 http://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.