Ternary Sql Alchemy

last modified: March 19, 2007

This is a sort of response to EjbTernaryRelationshipExample and OrTernaryRelationshipExample.

#!/usr/bin/env python2

import sqlalchemy as sa;

de = sa.create_engine("postgres:///3way")
md = sa.BoundMetaData(de)
#md.engine.echo = True

# inspect the table metadata
for t in ['suppliers', 'parts', 'projects',
          'r_projectsparts', 'r_suppliersparts', 'r_supplierspartsprojects']:
    globals()[t] = sa.Table(t, md, autoload=True)

# ORM session
sn = sa.create_session(bind_to=de)#, echo_uow=True)

# Full ORM, although it's not needed for this problem
class Supplier(object):
    def __init__(self, sname):
        self.supplier_name = sname # missing Ruby's Struct here
class Project(object):
    def __init__(self, pname):
        self.project_name = pname
class Part(object):
    def __init__(self, pcode):
        self.part_code = pcode
class SuppliedPart(object):
    def __init__(self, supp, part):
        self.part = part
        self.supplier = supp
class RequiredPart(object):
    def __init__(self, proj, part):
        self.part = part
        self.project = proj
class PotentialPart(object):
    def __init__(self, reqp, supp):
        # assuming you want this caught before the db sees it
        if reqp.part != supp.part:
            raise Exception("Parts mismatch")
        self.reqp = reqp
        self.supp = supp
class AcceptedPart(PotentialPart):
    def __init__(self, reqp, supp):
        PotentialPart.__init__(self, reqp, supp)

sa.mapper(Part, parts)
sa.mapper(Supplier, suppliers)
sa.mapper(Project, projects)
sa.mapper(SuppliedPart, r_suppliersparts, properties = {
    'part': sa.relation(Part, backref='splist', lazy=False),
    'supplier': sa.relation(Supplier, backref='splist', lazy=False)},)
sa.mapper(RequiredPart, r_projectsparts, properties = {
    'part': sa.relation(Part, backref='rplist', lazy=False),
    'project': sa.relation(Project, backref='rplist', lazy=False)},)
sa.mapper(AcceptedPart, r_supplierspartsprojects, properties = {
    'reqp': sa.relation(RequiredPart, backref=sa.backref('ap', uselist=False),
                        lazy=False), # 1:1, but may be None
    'supp': sa.relation(SuppliedPart, backref=sa.backref('ap', uselist=False),
                        lazy=False)},)

# this is more or less a client-side view
big_join = sa.join(projects, r_projectsparts).join(parts).               join(r_suppliersparts).join(suppliers) # problem with col names
named_join = sa.select([projects.c.project_name, parts.c.part_code,
                        suppliers.c.supplier_name], from_obj=[big_join]).                            alias('potparts')
# the fields are only needed if you want traversals or insertions
sa.mapper(PotentialPart, named_join, properties = {
    'reqp': sa.relation(RequiredPart, backref='pplist',
                        primaryjoin=sa.and_(named_join.c.part_code==
                                            r_projectsparts.c.part_code,
                                            named_join.c.project_name==
                                            r_projectsparts.c.project_name),
                        lazy=False),
    'supp': sa.relation(SuppliedPart, backref='pplist',
                        primaryjoin=sa.and_(named_join.c.part_code==
                                            r_suppliersparts.c.part_code,
                                            named_join.c.supplier_name==
                                            r_suppliersparts.c.supplier_name),
                        lazy=False)},)

# ORM ends here, insert some data now to show that it works as expected:
# all these assignments can be avoided with metaclasses, but KISS for now
# SA only checks shallow identity, so you'd need a weakref hash, yuck...
hammer                  = Part('hammer')
nails                   = Part('nails')
wood                    = Part('wood')
game                    = Part('game')
spice_rack              = Project('spice rack')
nice_meal               = Project('nice meal')
acme                    = Supplier('ACME')
forrest                 = Supplier('Forrest')
acme_hammer     = SuppliedPart(acme, hammer)
acme_nails              = SuppliedPart(acme, nails)
forrest_wood    = SuppliedPart(forrest, wood)
forrest_game    = SuppliedPart(forrest, game)
sr_hammer               = RequiredPart(spice_rack, hammer)
sr_wood                 = RequiredPart(spice_rack, wood)
sr_nails                = RequiredPart(spice_rack, nails)
nm_game                 = RequiredPart(nice_meal, game)
sn.save(AcceptedPart(sr_hammer, acme_hammer))
# ACME nails are substandard, so they're not accepted
## sn.save(AcceptedPart(sr_nails, acme_nails)) 
sn.save(AcceptedPart(sr_wood, forrest_wood))
sn.save(AcceptedPart(nm_game, forrest_game))

sn.flush() # commit

def parts_that_cannot(pname):
    """
    List all parts that suppliers cannot provide for the project
    and that the project actually requires, and only for those
    providers that provide at least one part; also count how many
    parts they *can* supply, and the total number of parts needed
    for the project: (total, [(supplier, [count, missing]), ...])
    """
    proj = sn.query(Project).selectone_by(project_name=pname)
    requires = frozenset((rp.part_code for rp in proj.rplist))
    hres = {},
    for part in sn.query(PotentialPart).select_by(project_name=pname):
        he = hres.setdefault(part.supplier_name, [0, []])
        he[0] = he[0] + 1
        he[1].append(part.part_code)
        hres[part.supplier_name] = he
    # do the set complement w/o SQL, probably more efficient
    # if most projects use only a small subset of the parts
    for hk, hv in hres.iteritems():
        hres[hk][1] = requires - frozenset(hv[1])
    res = hres.items()
    res.sort(key=lambda x: x[1][0], reverse=True)
    return (len(requires), res)

def parts_that_cannot_meth(self):
    return parts_that_cannot(self.project_name)

Project.suppliers_with_parts_they_cannot_provide = parts_that_cannot_meth

print "---spice rack---", parts_that_cannot('spice rack')
print "---nice meal---", nice_meal.suppliers_with_parts_they_cannot_provide() # OO way

Loading...