Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to specify table relationships in SQLAlchemy with multi-level/multiple joins?

I'm trying to define a relationship between two tables whose relations are indirect (i.e. through two other tables).

The results I'm looking for can be fetched with this query:

(db.session.query(Telnum)
           .filter(Account.customer==customer)
           .filter(Account.account_id == Subscription.account_id)
           .filter(Telnum.sub_id == Subscription.id)
           .order_by(Telnum.telnum)
           .all()
)

where customer is a Customer object.

I'm struggling to figure out how this would be defined as a relationship, similar to the Customer.invoices relationship. An idea I had was something like this:

telnums = db.relationship('Telnum',
                       primaryjoin="and_(Account.user_id==Customer.id, "
                       "Account.account_id == Subscription.account_id, " 
                       "Telnum.sub_id == Subscription.id)", 
                       backref='customer')

As evident by this post, that does not work. The error message it produces is this: sqlalchemy.exc.ArgumentError: Could not locate any simple equality expressions involving locally mapped foreign key columns for primary join condition 'accounts.user_id = customers.id AND accounts.account_id = subscriptions.account_id AND pstn_numbers.sub_id = subscriptions.id' on relationship Customer.telnums. Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or are annotated in the join condition with the foreign() annotation. To allow comparison operators other than '==', the relationship can be marked as viewonly=True.

Could anyone hint me in the right direction?

I have the following table structure (simplified, all irrelevant columns removed apart from one for each table):

class Customer(db.Model):
    __tablename__ = 'customers'
    id = db.Column(db.Integer, primary_key=True)
    identification_num = db.Column(db.String(10), unique=True)
    name = db.Column(db.Text)
    invoices = db.relationship('Invoice', backref='customer')
    accounts = db.relationship('Account', backref='customer')

def __init__(self):
    pass

def __repr__(self):
    return '<Customer %r>' % (self.name)

class Invoice(db.Model):
    __tablename__ = 'invoices'
    id = db.Column(db.Integer, primary_key=True)
    customer_id = db.Column(db.Integer, db.ForeignKey('customers.id'))
    active = db.Column(db.Boolean)
    accounts = db.relationship('Account', backref='invoice')

    def __repr__(self):
        return '<Invoice %r>' % (self.id)

class Account(db.Model):
    __tablename__ = 'accounts'
    id = db.Column(db.Integer, primary_key=True)
    account_id = db.Column(db.Integer, unique=True)
    invoice_id = db.Column(db.Integer, db.ForeignKey('invoices.id'))
    user_id = db.Column(db.Integer, db.ForeignKey('customers.id'))
    active = db.Column(db.Boolean)
    subscriptions = db.relationship('Subscription', backref='account')

    def __repr__(self):
        return '<Account %r>' % (self.account_id)

class Subscription(db.Model):
    __tablename__ = 'subscriptions'
    id = db.Column(db.Integer, primary_key=True)
    account_id = db.Column(db.Integer, db.ForeignKey('accounts.account_id'))
    sub_active = db.Column(db.DateTime)
    telnums = db.relationship('Telnum', backref='subscription')

    def __repr__(self):
        return '<Subscription %r>' % (self.id)

class Telnum(db.Model):
    __tablename__ = 'pstn_numbers'
    id = db.Column(db.Integer, primary_key=True)
    sub_id = db.Column(db.Integer, db.ForeignKey('subscriptions.id'))
    telnum = db.Column(db.String(64))
    holder = db.Column(db.String(10))

    def __repr__(self):
        return '<Telnum %r>' % (self.telnum)
like image 706
orn Avatar asked Mar 06 '14 15:03

orn


1 Answers

In general, I would not define an indirect relationship as a relationship, because you risk these indirect relationships becoming out-of-sync when you make modifications. You might work-around some of these limitations by specifying the viewonly=False parameter for a relationship.

A simpler, less risky, and more straight-forward solution would be to use a query (or query-enabled property) in case you would like to reload data from the database, and use python list comprehensions to get the sub-sub-children of the relationship tree:

class Customer(Base):
    # ...

    @property
    def telnums_qry(self):
        sess = Session.object_session(self)
        return (sess.query(Telnum)
                .join(Subscription)
                .join(Account)
                .filter(Account.user_id == self.id)
                ).all()


    @property
    def telnums_mem(self):
        return [tel
                for acc in self.accounts
                for sub in acc.subscriptions
                for tel in sub.telnums
                ]


class Telnum(Base):
    # ...

    @property
    def customer(self):
        return (self.subscription
                and self.subscription.account
                and self.subscription.account.customer
                )
like image 130
van Avatar answered Sep 20 '22 13:09

van