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

Revision 140, 8.7 kB (checked in by ged, 6 years ago)

- applied patch from Isaac Csandl to make associable relationships lazy by

default and be able to control laziness in the other direction.

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, 'addresses')
64   
65    class Person(Entity):
66        has_field('name', Unicode)
67        has_many('orders', of_kind='Order')
68        is_addressable()
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. Otherwise, the name will use the plural_name  |
100|               | provided to the associable call.                            |
101+---------------+-------------------------------------------------------------+
102| ``uselist``   | Whether or not the associated table should be represented   |
103|               | as a list, or a single property. It should be set to False  |
104|               | when the entity should only have a single associated        |
105|               | entity. Defaults to True.                                   |
106+---------------+-------------------------------------------------------------+
107| ``lazy``      | Determines eager loading of the associated entity objects.  |
108|               | Defaults to False, to indicate that they should not be      |
109|               | lazily loaded.                                              |
110+---------------+-------------------------------------------------------------+
111'''
112from elixir.statements import Statement
113import elixir as el
114import sqlalchemy as sa
115
116def associable(entity, plural_name=None, lazy=True):
117    '''
118    Generate an associable Elixir Statement
119    '''
120    interface_name = entity.table.name
121    able_name = interface_name + 'able'
122    attr_name = "%s_rel" % interface_name
123   
124    association_table = sa.Table("%s" % able_name, entity._descriptor.metadata,
125        sa.Column('%s_id' % able_name, sa.Integer, primary_key=True),
126        sa.Column('%s_type' % able_name, sa.String(40), nullable=False),
127    )
128   
129    association_to_table = sa.Table("%s_to_%s" % (able_name, interface_name), entity._descriptor.metadata,
130        sa.Column('%s_id' % able_name, sa.Integer, sa.ForeignKey(getattr(association_table.c, '%s_id' % able_name), ondelete="CASCADE"), primary_key=True),
131        sa.Column('%s_id' % interface_name, sa.Integer, sa.ForeignKey(entity.table.c.id, ondelete="RESTRICT"), primary_key=True),
132    )
133   
134    entity._assoc_table = association_table
135    entity._assoc_to_table = association_to_table
136    assoc_entity = entity
137    assoc_entity._assoc_relations = []
138   
139    def finder(key):
140        def find_by(cls, value):
141            pass
142        return find_by
143   
144    for col in entity.table.columns.keys():
145        if col != 'id':
146            setattr(entity, 'find_by_%s' % col, finder(col))
147   
148    class GenericAssoc(object):
149        def __init__(self, name):
150            setattr(self, '%s_type' % able_name, name)
151   
152    class Associable(el.relationships.Relationship):
153        """An associable Elixir Statement object"""
154        def __init__(self, entity, name=None, uselist=True, lazy=True):
155            self.entity = entity
156            self.lazy = lazy
157            self.uselist = uselist
158           
159            if name is None:
160                self.name = plural_name
161            else:
162                self.name = name
163           
164            assoc_entity._assoc_relations.append(entity)
165           
166            field = type('myfield', (object,), {})
167            field.colname = '%s_assoc_id' % interface_name
168            field.deferred = False
169            field.primary_key = False
170            field.column = sa.Column('%s_assoc_id' % interface_name, None, 
171                                  sa.ForeignKey('%s.%s_id' % (able_name, able_name)))
172            entity._descriptor.add_field(field)
173            entity._descriptor.relationships[able_name] = self
174           
175            def select_by(cls, **kwargs):
176                return cls.query().join(attr_name).join('targets').filter_by(**kwargs).list()
177            setattr(entity, 'select_by_%s' % self.name, classmethod(select_by))
178           
179            def select(cls, *args, **kwargs):
180                return cls.query().join(attr_name).join('targets').filter(*args, **kwargs).list()
181            setattr(entity, 'select_%s' % self.name, classmethod(select))
182       
183        def setup(self):
184            self.create_properties()
185            return True
186       
187        def create_properties(self):
188            entity = self.entity
189            entity.mapper.add_property(attr_name, sa.relation(GenericAssoc, lazy=self.lazy,
190                                       backref='_backref_%s' % entity.table.name))
191            entity.mapper.add_property(self.name, sa.synonym(attr_name))
192            if self.uselist:
193                def get(self):
194                    if getattr(self, attr_name) is None:
195                        setattr(self, attr_name, 
196                                GenericAssoc(entity.table.name))
197                    return getattr(self, attr_name).targets
198                setattr(entity, self.name, property(get))
199            else:
200                # scalar based property decorator
201                def get(self):
202                    attr = getattr(self, attr_name)
203                    if attr is not None:
204                        return attr.targets[0]
205                    else:
206                        return None
207                def set(self, value):
208                    if getattr(self, attr_name) is None:
209                        setattr(self, attr_name, 
210                                GenericAssoc(entity.table.name))
211                    getattr(self, attr_name).targets = [value]
212                setattr(entity, self.name, property(get, set))
213
214    sa.mapper(GenericAssoc, association_table, properties={
215        'targets': sa.relation(entity, secondary=association_to_table,
216                               lazy=lazy, backref='association',
217                               order_by=entity.mapper.order_by)
218    })
219    return Statement(Associable)
Note: See TracBrowser for help on using the browser.