On Jan 9, 2012, at 6:51 PM, jonstjohn wrote:

> To implement this I chose to use a closure table ala Bill Karwin
> (http://karwin.blogspot.com/2010/03/rendering-trees-with-closure-
> tables.html). In the closure table implementation, a second table is
> created to store the ancestor/descendent information. A self-
> referencing row is created when a node is added, as well as a row for
> each ancestor in the tree.

This is a schema design I've not seen before.  What's unusual here is that 
you'd like the "route" table to have something to do with the 
"area_relationship" table, however "area_relationship" has no foreign key to 
"route", nor vice versa.      This doesn't follow typical normalization rules, 
in that it's very easy to have rows in DbRoute that don't exist in 
area_relationship_table.  SQLAlchemy's ORM can't really navigate around a 
design like that and it's probably not really what you want here.

I checked the blog post you refer to and it does not have this pattern.  It has 
just a "node" table, which here corresponds to DbArea, and "closure", which 
here would be "area_relationship_table" and in SQLA we call it a 
self-referential many-to-many.   There's no "extra" table, so we need to figure 
out what you want there.

If it's the case that information such as "Leave it to Jesus", "Green Peace" 
are associated with a particular association between two areas, you would first 
need to promote area_relationship to be a fully mapped class, forming what we 
refer to as an "association object" 
(http://www.sqlalchemy.org/docs/orm/relationships.html#association-object) - a 
many-to-many table that contains additional information about the association.  
 Based on how you've named things here I think you'd then want to foreign key 
that table to DbRoute, so that DbRoute contains a collection of 
AreaRelationship associations.   You could also do something simpler which is 
just to add a string column to the AreaRelationship association table directly.

Introducing the association object usually leads to the usage of the 
association proxy to "hide" the middle object in most cases.    As this is 
likely to be a lot to take in, and there's also some complexity in getting the 
"routes" for an area, the attached script illustrates a mapping that seems to 
correspond to the sample data you have.  






> 
> 
> The table structure is as follows (simplified):
> 
> -- area --
> area_id
> name
> 
> -- area_relationship --
> ancestor
> descendent
> 
> -- route --
> route_id
> area_id
> name
> 
> 
> Sample data:
> 
> -- area --
> 1, New River Gorge
> 2, Kaymoor
> 3, South Nuttall
> 4, Meadow River Gorge
> 
> -- area_relationship (ancestor, descendent) --
> 1, 1 (self-referencing)
> 2, 2 (self-referencing)
> 1, 2 (Kaymoor is w/i New River Gorge)
> 3, 3 (self-referencing)
> 1, 3 (South Nutall is w/i New River Gorge)
> 4, 4 (self-referencing)
> 
> -- route (route_id, area_id, name)
> 1, 2, Leave it to Jesus
> 2, 2, Green Piece
> 3, 4, Fancy Pants
> 
> 
> To query for all areas for a given route (up the tree), I can execute:
> 
> SELECT area.area_id, area.name
> FROM route
>    INNER JOIN area_relationship ON route.area_id =
> area_relationship.descendent
>    INNER JOIN area ON area.area_id = area_relationship.ancestor
> WHERE route.route_id = 1
> Similarly, I can query for all routes in a particular area (including
> descendent areas) with:
> 
> SELECT route.route_id, route.name
> FROM area
>    INNER JOIN area_relationship ON area.area_id =
> area_relationship.ancestor
>    INNER JOIN route ON route.area_id = area_relationship.descendent
> WHERE area.area_id = 1
> 
> 
> In SQL Alchemy I've created a relationship and two tables to handle
> these relationships:
> 
> area_relationship_table = Table('area_relationship', Base.metadata,
>  Column('ancestor', Integer, ForeignKey('area.area_id')),
>  Column('descendent', Integer, ForeignKey('area.area_id'))
> )
> 
> 
> DbArea class -
> 
> class DbArea(Base):
> 
>    __tablename__ = 'area'
> 
>    area_id = Column(Integer, primary_key = True)
>    name = Column(VARCHAR(50))
>    created = Column(DATETIME)
> 
>    area_relationship_table.c.ancestor])
> 
>    descendents = relationship('DbArea', backref = 'ancestors',
>        secondary =  area_relationship_table,
>        primaryjoin = area_id == area_relationship_table.c.ancestor,
>        secondaryjoin = area_id ==
> area_relationship_table.c.descendent)
> 
> 
> DbRoute class -
> 
> class DbRoute(Base):
> 
>        __tablename__ = 'route'
> 
>        route_id = Column(Integer, primary_key = True)
>        area_id = Column(Integer, ForeignKey('area.area_id'))
>        name = Column(VARCHAR(50))
>        created = Column(DATETIME)
> 
>        area = relationship("DbArea")
> 
>        areas = relationship('DbArea', backref = 'routes',
>            secondary = area_relationship_table,
>            primaryjoin = area_id ==
> area_relationship_table.c.ancestor,
>            secondaryjoin = area_id ==
> area_relationship_table.c.descendent,
>            foreign_keys=[area_relationship_table.c.ancestor,
>            area_relationship_table.c.descendent])
> 
> 
> Currently, I am able to determine the areas from the individual route,
> using the areas relationship in DbRoute. However, when I try to use
> the backref 'routes' in DbArea, I get the following error:
> 
> sqlalchemy.exc.StatementError: No column route.area_id is configured
> on mapper Mapper|DbArea|area... (original cause: UnmappedColumnError:
> No column route.area_id is configured on mapper Mapper|DbArea|area...)
> 'SELECT route.route_id AS route_route_id, route.area_id AS
> route_area_id, route.name AS route_name, route.created AS
> route_created \nFROM route, area_relationship \nWHERE %s =
> area_relationship.descendent AND route.area_id =
> area_relationship.ancestor' [immutabledict({})]
> 
> I'm guessing that I likely need to add something to DbArea to
> establish the relationship, but after experimenting with some
> different options was unable to determine the solution.
> 
> -- 
> You received this message because you are subscribed to the Google Groups 
> "sqlalchemy" group.
> To post to this group, send email to sqlalchemy@googlegroups.com.
> To unsubscribe from this group, send email to 
> sqlalchemy+unsubscr...@googlegroups.com.
> For more options, visit this group at 
> http://groups.google.com/group/sqlalchemy?hl=en.
> 

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy

Base= declarative_base()



class DbArea(Base):

    __tablename__ = 'area'

    area_id = Column(Integer, primary_key = True)
    name = Column(VARCHAR(50))
    created = Column(DATETIME)

    descendent_rels = relationship('AreaRelationship', backref = 'ancestor',
       primaryjoin = "DbArea.area_id == AreaRelationship.ancestor_id")
    ancestor_rels = relationship('AreaRelationship', backref='descendent',
       primaryjoin = "DbArea.area_id == AreaRelationship.descendent_id")

    ancestors = association_proxy("anscestor_rels", "ancestor")
    descendents = association_proxy("descendent_rels", "descendent")

    @property
    def routes(self):
        return [ar.route for ar in self.ancestor_rels] + [
            ar.route for ar in self.descendent_rels
            ]

class AreaRelationship(Base):
    __tablename__ = 'area_relationship'
    ancestor_id = Column(Integer, ForeignKey('area.area_id'), primary_key=True)
    descendent_id = Column(Integer, ForeignKey('area.area_id'), primary_key=True)
    route_id = Column(Integer, ForeignKey('route.route_id'))
    route = relationship("DbRoute", backref="area_rels")

class DbRoute(Base):

    __tablename__ = 'route'

    route_id = Column(Integer, primary_key = True)
    name = Column(VARCHAR(50))

    @property
    def areas(self):
        return [ar.ancestor for ar in self.area_rels] + [
            ar.descendent for ar in self.area_rels
            ]

e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)

s = Session(e)

a1, a2, a3, a4 = DbArea(name="New River Gorge"),\
                        DbArea(name="Kaymoor"),\
                        DbArea(name="South Nuttall"),\
                        DbArea(name="Meadow River Gorge")


r1, r2, r3 = DbRoute(name="Leave it to Jesus"), DbRoute(name="Green Piece"), DbRoute(name="Fancy Pants")
s.add_all([
    AreaRelationship(ancestor=a1, descendent=a1),
    AreaRelationship(ancestor=a2, descendent=a2, route=r2),
    AreaRelationship(ancestor=a1, descendent=a2, route=r1),
    AreaRelationship(ancestor=a3, descendent=a3),
    AreaRelationship(ancestor=a1, descendent=a3),
])

s.commit()

print s.query(DbRoute).filter_by(name="Green Piece").one().areas
-- 
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To post to this group, send email to sqlalchemy@googlegroups.com.
To unsubscribe from this group, send email to 
sqlalchemy+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/sqlalchemy?hl=en.

Reply via email to