root / elixir / trunk / elixir / ext / associable.py @ 123

Revision 123, 8.4 kB (checked in by ged, 6 years ago)

- Apply suggestion from Isaac Csandl:

If the table of the associable object has an order_by clause, it will
be respected when you access the property.

Line 
1'''
2Associable Elixir Statement Generator
3
4==========
5Associable
6==========
7
8About Polymorphic Associations
9------------------------------
10
11A frequent pattern in database schemas is the has_and_belongs_to_many, or a
12many-to-many table. Quite often multiple tables will refer to a single one
13creating quite a few many-to-many intermediate tables.
14
15Polymorphic associations lower the amount of many-to-many tables by setting up
16a table that allows relations to any other table in the database, and relates it
17to the associable table. In some implementations, this layout does not enforce
18referential integrity with database foreign key constraints, this implementation
19uses an additional many-to-many table with foreign key constraints to avoid
20this problem.
21
22.. note:
23    SQLite does not support foreign key constraints, so referential integrity
24    can only be enforced using database backends with such support.
25
26Elixir Statement Generator for Polymorphic Associations
27-------------------------------------------------------
28
29The ``associable`` function generates the intermediary tables for an Elixir
30entity that should be associable with other Elixir entities and returns an
31Elixir Statement for use with them. This automates the process of creating the
32polymorphic association tables and ensuring their referential integrity.
33
34Matching select_XXX and select_by_XXX are also added to the associated entity
35which allow queries to be run for the associated objects.
36
37Example usage:
38
39::
40   
41    class Tag(Entity):
42        has_field('name', Unicode)
43   
44    acts_as_taggable = associable(Tag)
45   
46    class Entry(Entity):
47        has_field('title', Unicode)
48        acts_as_taggable('tags')
49   
50    class Article(Entity):
51        has_field('title', Unicode)
52        acts_as_taggable('tags')
53
54Or if one of the entities being associated should only have a single member of
55the associated table:
56
57::
58   
59    class Address(Entity):
60        has_field('street', String(130))
61        has_field('city', String)
62   
63    is_addressable = associable(Address)
64   
65    class Person(Entity):
66        has_field('name', Unicode)
67        has_many('orders', of_kind='Order')
68        is_addressable('addresses')
69   
70    class Order(Entity):
71        has_field('order_num', primary_key=True)
72        has_field('item_count', Integer)
73        belongs_to('person', of_kind='Person')
74        is_addressable('address', uselist=False)
75   
76    home = Address(street='123 Elm St.', city='Spooksville')
77    user = Person(name='Jane Doe')
78    user.addresses.append(home)
79   
80    neworder = Order(item_count=4)
81    neworder.address = home
82    user.orders.append(neworder)
83   
84    # Queries using the added helpers
85    Person.select_by_addresses(city='Cupertino')
86    Person.select_addresses(and_(Address.c.street=='132 Elm St',
87                                 Address.c.city=='Smallville'))
88
89Statement Options
90-----------------
91
92The generated Elixir Statement has several options available:
93
94+---------------+-------------------------------------------------------------+
95| Option Name   | Description                                                 |
96+===============+=============================================================+
97| ``name``      | Specify a custom name for the Entity attribute. This is     |
98|               | used to declare the attribute used to access the associated |
99|               | table values                                                |
100+---------------+-------------------------------------------------------------+
101| ``uselist``   | Whether or not the associated table should be represented   |
102|               | as a list, or a single property. It should be set to False  |
103|               | when the entity should only have a single associated        |
104|               | entity. Defaults to True.                                   |
105+---------------+-------------------------------------------------------------+
106| ``lazy``      | Determines eager loading of the associated entity objects.  |
107|               | Defaults to False, to indicate that they should not be      |
108|               | lazily loaded.                                              |
109+---------------+-------------------------------------------------------------+
110'''
111from elixir.statements import Statement
112import elixir as el
113import sqlalchemy as sa
114
115def associable(entity):
116    '''
117    Generate an associable Elixir Statement
118    '''
119    interface_name = entity.table.name
120    able_name = interface_name + 'able'
121    attr_name = "%s_rel" % interface_name
122   
123    association_table = sa.Table("%s" % able_name, entity._descriptor.metadata,
124        sa.Column('%s_id' % able_name, sa.Integer, primary_key=True),
125        sa.Column('%s_type' % able_name, sa.String(40), nullable=False),
126    )
127   
128    association_to_table = sa.Table("%s_to_%s" % (able_name, interface_name), entity._descriptor.metadata,
129        sa.Column('%s_id' % able_name, sa.Integer, sa.ForeignKey(getattr(association_table.c, '%s_id' % able_name), ondelete="CASCADE"), primary_key=True),
130        sa.Column('%s_id' % interface_name, sa.Integer, sa.ForeignKey(entity.table.c.id, ondelete="RESTRICT"), primary_key=True),
131    )
132   
133    entity._assoc_table = association_table
134    entity._assoc_to_table = association_to_table
135    assoc_entity = entity
136    assoc_entity._assoc_relations = []
137   
138    def finder(key):
139        def find_by(cls, value):
140            pass
141        return find_by
142   
143    for col in entity.table.columns.keys():
144        if col != 'id':
145            setattr(entity, 'find_by_%s' % col, finder(col))
146   
147    class GenericAssoc(object):
148        def __init__(self, name):
149            setattr(self, '%s_type' % able_name, name)
150   
151    class Associable(el.relationships.Relationship):
152        """An associable Elixir Statement object"""
153        def __init__(self, entity, name, uselist=True, lazy=False):
154            self.entity = entity
155            self.name = name
156            self.lazy = lazy
157            self.uselist = uselist
158            assoc_entity._assoc_relations.append(entity)
159           
160            field = type('myfield', (object,), {})
161            field.colname = '%s_assoc_id' % interface_name
162            field.deferred = False
163            field.primary_key = False
164            field.column = sa.Column('%s_assoc_id' % interface_name, None, 
165                                  sa.ForeignKey('%s.%s_id' % (able_name, able_name)))
166            entity._descriptor.add_field(field)
167            entity._descriptor.relationships[able_name] = self
168           
169            def select_by(cls, **kwargs):
170                return cls.query().join(attr_name).join('targets').filter_by(**kwargs).list()
171            setattr(entity, 'select_by_%s' % self.name, classmethod(select_by))
172           
173            def select(cls, *args, **kwargs):
174                return cls.query().join(attr_name).join('targets').filter(*args, **kwargs).list()
175            setattr(entity, 'select_%s' % self.name, classmethod(select))
176       
177        def setup(self):
178            self.create_properties()
179            return True
180       
181        def create_properties(self):
182            entity = self.entity
183            entity.mapper.add_property(attr_name, sa.relation(GenericAssoc, lazy=self.lazy,
184                                       backref='_backref_%s' % entity.table.name))
185            entity.mapper.add_property(self.name, sa.synonym(attr_name))
186            if self.uselist:
187                def get(self):
188                    if getattr(self, attr_name) is None:
189                        setattr(self, attr_name, 
190                                GenericAssoc(entity.table.name))
191                    return getattr(self, attr_name).targets
192                setattr(entity, self.name, property(get))
193            else:
194                # scalar based property decorator
195                def get(self):
196                    return getattr(self, attr_name).targets[0]
197                def set(self, value):
198                    if getattr(self, attr_name) is None:
199                        setattr(self, attr_name, 
200                                GenericAssoc(entity.table.name))
201                    getattr(self, attr_name).targets = [value]
202                setattr(entity, self.name, property(get, set))
203
204    sa.mapper(GenericAssoc, association_table, properties={
205        'targets': sa.relation(entity, secondary=association_to_table,
206                               lazy=False, backref='association',
207                               order_by=entity.mapper.order_by)
208    })
209    return Statement(Associable)
Note: See TracBrowser for help on using the browser.