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

Revision 263, 8.8 kB (checked in by ged, 7 years ago)
  • minor style improvements, correct typos, ...
  • bump version to 0.4.1
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.. sourcecode:: python
40   
41    class Tag(Entity):
42        name = Field(Unicode)
43   
44    acts_as_taggable = associable(Tag)
45   
46    class Entry(Entity):
47        title = Field(Unicode)
48        acts_as_taggable('tags')
49   
50    class Article(Entity):
51        title = Field(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.. sourcecode:: python
58   
59    class Address(Entity):
60        street = Field(String(130))
61        city = Field(String)
62   
63    is_addressable = associable(Address, 'addresses')
64   
65    class Person(Entity):
66        name = Field(Unicode)
67        orders = OneToMany('Order')
68        is_addressable()
69   
70    class Order(Entity):
71        order_num = Field(primary_key=True)
72        item_count = Field(Integer)
73        person = ManyToOne('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
116__doc_all__ = ['associable']
117
118def associable(assoc_entity, plural_name=None, lazy=True):
119    '''
120    Generate an associable Elixir Statement
121    '''
122    interface_name = assoc_entity._descriptor.tablename
123    able_name = interface_name + 'able'
124
125    if plural_name:
126        attr_name = "%s_rel" % plural_name
127    else:
128        plural_name = interface_name
129        attr_name = "%s_rel" % interface_name
130
131    class GenericAssoc(object):
132        def __init__(self, tablename):
133            self.type = tablename
134   
135    #TODO: inherit from entity builder
136    class Associable(object):
137        """An associable Elixir Statement object"""
138        def __init__(self, entity, name=None, uselist=True, lazy=True):
139            self.entity = entity
140            self.lazy = lazy
141            self.uselist = uselist
142           
143            if name is None:
144                self.name = plural_name
145            else:
146                self.name = name
147
148        def after_table(self):
149            col = sa.Column('%s_assoc_id' % interface_name, sa.Integer, 
150                            sa.ForeignKey('%s.id' % able_name))
151            self.entity._descriptor.add_column(col)
152
153            if not hasattr(assoc_entity, '_assoc_table'):
154                association_table = sa.Table("%s" % able_name, assoc_entity._descriptor.metadata,
155                    sa.Column('id', sa.Integer, primary_key=True),
156                    sa.Column('type', sa.String(40), nullable=False),
157                )
158               
159                association_to_table = sa.Table("%s_to_%s" % (able_name, interface_name), assoc_entity._descriptor.metadata,
160                    sa.Column('assoc_id', sa.Integer, sa.ForeignKey(association_table.c.id, ondelete="CASCADE"), primary_key=True),
161                    #FIXME: this assumes a single id col
162                    sa.Column('%s_id' % interface_name, sa.Integer, sa.ForeignKey(assoc_entity.table.c.id, ondelete="RESTRICT"), primary_key=True),
163                )
164
165                assoc_entity._assoc_table = association_table
166                assoc_entity._assoc_to_table = association_to_table
167
168        def after_mapper(self):
169            if not hasattr(assoc_entity, '_assoc_mapper'):
170                assoc_entity._assoc_mapper = sa.orm.mapper(
171                    GenericAssoc, assoc_entity._assoc_table, properties={
172                        'targets': sa.orm.relation(
173                                       assoc_entity,
174                                       secondary=assoc_entity._assoc_to_table,
175                                       lazy=lazy, backref='associations',
176                                       order_by=assoc_entity.mapper.order_by)
177                })
178       
179            entity = self.entity
180            entity.mapper.add_property(
181                attr_name, 
182                sa.orm.relation(GenericAssoc, lazy=self.lazy,
183                                backref='_backref_%s' % entity.table.name)
184            )
185
186            # this is strange! self.name is both set via mapper synonym and
187            # the python property
188            entity.mapper.add_property(self.name, sa.orm.synonym(attr_name))
189
190            if self.uselist:
191                def get(self):
192                    if getattr(self, attr_name) is None:
193                        setattr(self, attr_name, 
194                                GenericAssoc(entity.table.name))
195                    return getattr(self, attr_name).targets
196                setattr(entity, self.name, property(get))
197            else:
198                # scalar based property decorator
199                def get(self):
200                    attr = getattr(self, attr_name)
201                    if attr is not None:
202                        return attr.targets[0]
203                    else:
204                        return None
205                def set(self, value):
206                    if getattr(self, attr_name) is None:
207                        setattr(self, attr_name, 
208                                GenericAssoc(entity.table.name))
209                    getattr(self, attr_name).targets = [value]
210                setattr(entity, self.name, property(get, set))
211
212            # add helper methods
213            def select_by(cls, **kwargs):
214                return cls.query.join([attr_name, 'targets']).filter_by(**kwargs).all()
215            setattr(entity, 'select_by_%s' % self.name, classmethod(select_by))
216           
217            def select(cls, *args, **kwargs):
218                return cls.query.join([attr_name, 'targets']).filter(*args, **kwargs).all()
219            setattr(entity, 'select_%s' % self.name, classmethod(select))
220
221    return Statement(Associable)
Note: See TracBrowser for help on using the browser.