2017-12-03 4 views
0

ich folgt ist ein minimales Beispiel für eine Viele-zu-viele-Assoziation. Mein Ziel ist es, einen einzelnen Datensatz von X und auch laden eifrig die Instanzen von Y, die in der ys Liste Rekord zu laden sowie Instanzen X die xs Liste in einem dieser Instanz sind.Eagerly laden eine kreisförmige Assoziation

class X(db.Model): 
    __tablename__ = 'x' 
    xid = db.Column(db.Integer, primary_key=True) 
    ys = relationship('Z', back_populates='x', lazy='joined') 


class Y(db.Model): 
    __tablename__ = 'y' 
    yid = db.Column(db.Integer, primary_key=True) 
    xs = relationship('Z', back_populates='y', lazy='joined') 


class Z(db.Model): 
    __tablename__ = 'z' 
    xid = db.Column(db.Integer, db.ForeignKey('x.xid'), primary_key=True) 
    yid = db.Column(db.Integer, db.ForeignKey('y.yid'), primary_key=True) 
    x = relationship('X', back_populates='ys', lazy='joined') 
    y = relationship('Y', back_populates='xs', lazy='joined') 

Mein Ziel ist es, das folgende Ergebnis zu erzeugen:

expected = [{ 
    'xid': 1, 
    'ys': [ 
     {'yid': 101, 'xs': [{'xid': 1}, {'xid': 2}, {'xid': 3}]}, 
     {'yid': 102, 'xs': [{'xid': 1}, {'xid': 2}]}, 
     {'yid': 104, 'xs': [{'xid': 1}, {'xid': 4}]}, 
    ], 
}] 

Die SQL-Anweisung, dies zu erreichen, ist recht unkompliziert:

SELECT x.xid, y.yid, x2.xid FROM x 
JOIN z  ON z.xid = x.xid JOIN y  ON z.yid = y.yid ; Fetch Ys 
JOIN z as z2 ON z2.yid = y.yid JOIN x as x2 ON z2.xid = x2.xid ; Fetch Xs (depth 2) 
WHERE x.xid = 1 

Mein Problem ist die Bestimmung, wie eine SQLAlchemy Abfrage erstellen Das wird entweder (a) mir erlauben, diese unformatierte Abfrage auszuführen und sie korrekt den richtigen Modellinstanzen zuzuordnen, oder (b) die Abfrage massieren (mit einer gewissen Kombination von join und contains_eager call) s), so dass es weiß, wie man eigentlich die Joins verwendet, die es generiert, so dass es nicht in n + 1 Abfragen explodiert.

Die richtige Abfrage wird durch die folgenden generiert, aber ich kann nicht schaffen, die Tiefe 2 X-Instanzen von dieser Abfrage zu laden (die Daten werden träge durch eine zweite Auswahl geladen).

a = aliased(Z) 
b = aliased(X) 
q = X.query.filter(X.xid==1).join(X.ys).join(Z.y).join(a, Y.xs).join(b, Z.x) 

Antwort

4

Die Art und Weise der eifrige-Lademechanismus funktioniert, ist, Ihnen einen Weg der Beziehung angeben, müssen Sie auch laden möchten, wie, wie Sie es laden wollen. Der Pfad ist im Grunde, welche Beziehungen nacheinander folgen, um die gewünschte Beziehung zu finden. In Ihrem speziellen Beispiel ist die richtige Sache zu tun:

q = session.query(X).filter(X.xid == 1) \ 
      .join(X.ys) \ 
      .join(Z.y) \ 
      .join(a, Y.xs) \ 
      .join(b, Z.x) \ 
      .options(
       contains_eager(X.ys), 
       contains_eager(X.ys, Z.y), 
       contains_eager(X.ys, Z.y, Y.xs, alias=a), 
       contains_eager(X.ys, Z.y, Y.xs, Z.x, alias=b), 
      ) 

Jede contains_eager eine Last auf einer einzigen Beziehung angibt, mit dem Pfad (X.ys, Z.y, Y.xs, Z.x) angeben, wo die Beziehung ist, und die contains_eager sowie alias Spezifizierungs wie man die Beziehung lädt. Das ist ziemlich ausführlich, aber zum Glück bietet SQLAlchemy eine Verknüpfung, die Sie zusammen ketten sie können, wie folgt aus:

.options(contains_eager(X.ys).contains_eager(Z.y).contains_eager(Y.xs, alias=a).contains_eager(Z.x, alias=b)) 

Wenn Sie .join für das explizite Ziel, dann tun contains_eager verwenden, könnte man genauso gut verwenden joinedload statt:

q = session.query(X).filter(X.xid==1) \ 
      .options(joinedload(X.ys).joinedload(Z.y).joinedload(Y.xs).joinedload(Z.x)) 

In Ihrem speziellen Fall, wie diese Verbindung nicht effizient sein kann, wenn Ihre Branche Faktor hoch ist, das heißt, wenn Ihre X.ys und Y.xs maximal n Einträge enthalten, dann Ihre Datenbank hat IhnenschickenKopien von jede einzelne Zeile in X. Aus diesem Grund ist subqueryload oft die richtige Wahl für Eins-zu-Viele-Beziehungen (dies ist nicht immer der Fall; der Kompromiss besteht zwischen der Anzahl von Abfragen, dh Latenz, gegenüber der Datenmenge in jeder Abfrage, dh Durchsatz, so Profil erfahren):

q = session.query(X).filter(X.xid==1) \ 
      .options(subqueryload(X.ys).joinedload(Z.y).subqueryload(Y.xs).joinedload(Z.x)) 

Schließlich, wenn alles, was Sie wollen, ist eine many-to-many-Beziehung, warum nicht nur eine many-to-many-Beziehung in erster Linie konfigurieren?

+0

Ehrfürchtig - mein Problem war, dass ich nur den Aufruf _last_ 'contains_ager 'verwendete, den Sie im ersten Beispiel haben. Ich dachte, das wäre genug, um den Pfad zu kodieren. – efritz

Verwandte Themen