On Thu, Jan 23, 2014 at 7:46 PM, Michael Bayer <[email protected]> wrote:
>
> On Jan 23, 2014, at 2:07 PM, Simon King <[email protected]> wrote:
>
>>
>> That's fantastic, thanks so much. I feel bad that my silly use case
>> has caused so much work for you and grown the docs even more (perhaps
>> you need a separate "Tricks for People who Should Know Better"
>> section)
>
> oh but this use case comes up all the time, it's now that SQLA can really 
> write a decent SELECT for this kind of thing that I felt it was time to make 
> it an official use case on the site.
>
>>
>> If I understand what you've written correctly, the non-primary-mapper
>> version is the only one that will meet my needs. It seems to be
>> working well, both lazy and eager loading, and the extra properties
>> appearing on the class aren't an issue (I'm marking a lot of them as
>> deferred so as not to load too much from the database).
>
> Right, I think the differentiating factor when NP is needed is that you're 
> joining A->B, there's any number of C, D in the middle, but also A and B have 
> some direct foreign keys as well.  is that the case for you?
>
> the non_primary use case could still benefit from some cleanup; the column 
> naming issue as well as the fact that you get those extra columns in your 
> results.

For anyone interested, here is roughly what I ended up with (full test
script attached):

import sqlalchemy as sa
import sqlalchemy.orm as saorm
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Terms(Base):
    __tablename__ = 'terms'
    id = sa.Column(sa.Integer(), primary_key=True)
    version = sa.Column(sa.Integer(), default=1)
    terms = sa.Column(sa.UnicodeText())

class Company(Base):
    __tablename__ = 'company'
    id = sa.Column(sa.Integer(), primary_key=True)
    name = sa.Column(sa.Unicode(60))
    termsid = sa.Column(sa.Integer, sa.ForeignKey(Terms.id),
                        nullable=False)
    terms = saorm.relationship(Terms)

class User(Base):
    __tablename__ = 'user'
    id = sa.Column(sa.Integer(), primary_key=True)
    name = sa.Column(sa.Unicode(60))
    companyid = sa.Column(sa.Integer, sa.ForeignKey(Company.id),
                          nullable=False)
    company = saorm.relationship(Company, backref='members')

class Acceptance(Base):
    __tablename__ = 'acceptance'
    userid = sa.Column(sa.Integer(), sa.ForeignKey(User.id),
                       primary_key=True)
    termsid = sa.Column(sa.Integer(), sa.ForeignKey(Terms.id),
                        primary_key=True)
    termsversion = sa.Column(sa.Integer(), nullable=False)

    user = saorm.relationship(User)
    terms = saorm.relationship(Terms)

j = (saorm.join(Company, Terms)
     .join(Acceptance,
           sa.and_(Acceptance.termsid == Terms.id,
                   Acceptance.termsversion == Terms.version)
           )
     )
currentacceptance = saorm.mapper(
    Acceptance,
    j,
    properties={
        'companyid': j.c.company_id,
        'termsid': [j.c.terms_id,
                    j.c.company_termsid,
                    j.c.acceptance_termsid],
    },
    non_primary=True)

User.currentacceptance = saorm.relationship(
    currentacceptance,
    primaryjoin=sa.and_(
        User.id == saorm.foreign(j.c.acceptance_userid),
        User.companyid == j.c.company_id,
        ),
    viewonly=True,
    uselist=False)


Thanks again to Mike for all your help,

Simon

-- 
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/groups/opt_out.
import sqlalchemy as sa
import sqlalchemy.orm as saorm
from sqlalchemy.event import listen
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Terms(Base):
    __tablename__ = 'terms'
    id = sa.Column(sa.Integer(), primary_key=True)
    version = sa.Column(sa.Integer(), default=1)
    terms = sa.Column(sa.UnicodeText())

class Company(Base):
    __tablename__ = 'company'
    id = sa.Column(sa.Integer(), primary_key=True)
    name = sa.Column(sa.Unicode(60))
    termsid = sa.Column(sa.Integer, sa.ForeignKey(Terms.id),
                        nullable=False)
    terms = saorm.relationship(Terms)

class User(Base):
    __tablename__ = 'user'
    id = sa.Column(sa.Integer(), primary_key=True)
    name = sa.Column(sa.Unicode(60))
    companyid = sa.Column(sa.Integer, sa.ForeignKey(Company.id),
                          nullable=False)
    company = saorm.relationship(Company, backref='members')

class Acceptance(Base):
    __tablename__ = 'acceptance'
    userid = sa.Column(sa.Integer(), sa.ForeignKey(User.id),
                       primary_key=True)
    termsid = sa.Column(sa.Integer(), sa.ForeignKey(Terms.id),
                        primary_key=True)
    termsversion = sa.Column(sa.Integer(), nullable=False)

    user = saorm.relationship(User)
    terms = saorm.relationship(Terms)

j = (saorm.join(Company, Terms)
     .join(Acceptance,
           sa.and_(Acceptance.termsid == Terms.id,
                   Acceptance.termsversion == Terms.version)
           )
     )
currentacceptance = saorm.mapper(
    Acceptance,
    j,
    properties={
        'companyid': j.c.company_id,
        'termsid': [j.c.terms_id,
                    j.c.company_termsid,
                    j.c.acceptance_termsid],
    },
    non_primary=True)

User.currentacceptance = saorm.relationship(
    currentacceptance,
    primaryjoin=sa.and_(
        User.id == saorm.foreign(j.c.acceptance_userid),
        User.companyid == j.c.company_id,
        ),
    viewonly=True,
    uselist=False)

def setup(session):
    terms = [
        Terms(version=1),
        Terms(version=1),
        ]
    session.add_all(terms)
    companies = [
        Company(name=u'Company 1', terms=terms[0]),
        Company(name=u'Company 2', terms=terms[0]),
        Company(name=u'Company 3', terms=terms[1]),
        ]
    session.add_all(companies)
    for i in range(9):
        company = companies[i%3]
        user = User(name=u'User %s' % (i + 1),
                    company=company)
        session.add(user)
        # some users should have accepted the terms
        if i % 3:
            acceptance = Acceptance(user=user,
                                    terms=company.terms,
                                    termsversion=company.terms.version)
            session.add(acceptance)

def display(loadtype, title, session):
    print
    print title
    print '=' * len(title)
    q = (session.query(User)
         .options(saorm.joinedload_all('company.terms')))
    if loadtype == 'joined':
        q = q.options(saorm.joinedload('currentacceptance'))
    elif loadtype == 'subquery':
        q = q.options(saorm.subqueryload('currentacceptance'))
    for user in q:
        print ('%s of %s: %s'
               % (user.name,
                  user.company.name,
                  user.currentacceptance and 'accepted' or 'not accepted'))

def test():
    from optparse import OptionParser
    parser = OptionParser()
    parser.add_option('-v', '--verbose', action='count',
                      default=0)
    parser.add_option('-t', '--loadtype',
                      type='choice',
                      choices=['lazy', 'joined', 'subquery'],
                      default='joined')
    options, args = parser.parse_args()

    echo = {0: False,
            1: True}.get(options.verbose, 'debug')

    engine = sa.create_engine('sqlite:///:memory:', echo=echo)
    queries = [0]
    def before_cursor_execute(conn, cursor, statement, *args):
        if statement.startswith('SELECT'):
            queries[0] += 1
    listen(engine, 'before_cursor_execute', before_cursor_execute)

    Session = saorm.sessionmaker(bind=engine)
    Base.metadata.create_all(engine)

    session = Session()
    setup(session)
    session.flush()
    display(options.loadtype,
            'Initial Setup',
            session)
    session.commit()

    t1 = session.query(Terms).get(1)
    t1.version += 1
    session.flush()
    display(options.loadtype,
            'After bumping version of terms',
            session)
    session.commit()

    c3 = session.query(Company).get(3)
    c3.termsid = 1
    session.flush()
    display(options.loadtype,
            'After pointing company 3 at different terms',
            session)

    print
    print '%s queries' % queries[0]


if __name__ == '__main__':
    test()

Reply via email to