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

Revision 267, 9.1 kB (checked in by ged, 6 years ago)
  • cleanup class attributes (in the attributes-based syntax) after the
    property is attached to its entity, so that SQLAlchemy is not confused.
    Only caused problem in the case of single inheritance and when omitting
    some values. See SA ticket #866.
  • some PEP8 fixes
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
17it to the associable table. In some implementations, this layout does not
18enforce referential integrity with database foreign key constraints, this
19implementation uses an additional many-to-many table with foreign key
20constraints to avoid this 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
118
119def associable(assoc_entity, plural_name=None, lazy=True):
120    '''
121    Generate an associable Elixir Statement
122    '''
123    interface_name = assoc_entity._descriptor.tablename
124    able_name = interface_name + 'able'
125
126    if plural_name:
127        attr_name = "%s_rel" % plural_name
128    else:
129        plural_name = interface_name
130        attr_name = "%s_rel" % interface_name
131
132    class GenericAssoc(object):
133   
134        def __init__(self, tablename):
135            self.type = tablename
136   
137    #TODO: inherit from entity builder
138    class Associable(object):
139        """An associable Elixir Statement object"""
140       
141        def __init__(self, entity, name=None, uselist=True, lazy=True):
142            self.entity = entity
143            self.lazy = lazy
144            self.uselist = uselist
145           
146            if name is None:
147                self.name = plural_name
148            else:
149                self.name = name
150
151        def after_table(self):
152            col = sa.Column('%s_assoc_id' % interface_name, sa.Integer, 
153                            sa.ForeignKey('%s.id' % able_name))
154            self.entity._descriptor.add_column(col)
155
156            if not hasattr(assoc_entity, '_assoc_table'):
157                metadata = assoc_entity._descriptor.metadata
158                association_table = sa.Table("%s" % able_name, metadata,
159                    sa.Column('id', sa.Integer, primary_key=True),
160                    sa.Column('type', sa.String(40), nullable=False),
161                )
162                tablename =  "%s_to_%s" % (able_name, interface_name)
163                association_to_table = sa.Table(tablename, metadata,
164                    sa.Column('assoc_id', sa.Integer, 
165                              sa.ForeignKey(association_table.c.id, 
166                                            ondelete="CASCADE"), 
167                              primary_key=True),
168                    #FIXME: this assumes a single id col
169                    sa.Column('%s_id' % interface_name, sa.Integer, 
170                              sa.ForeignKey(assoc_entity.table.c.id, 
171                                            ondelete="RESTRICT"), 
172                              primary_key=True),
173                )
174
175                assoc_entity._assoc_table = association_table
176                assoc_entity._assoc_to_table = association_to_table
177
178        def after_mapper(self):
179            if not hasattr(assoc_entity, '_assoc_mapper'):
180                assoc_entity._assoc_mapper = sa.orm.mapper(
181                    GenericAssoc, assoc_entity._assoc_table, properties={
182                        'targets': sa.orm.relation(
183                                       assoc_entity,
184                                       secondary=assoc_entity._assoc_to_table,
185                                       lazy=lazy, backref='associations',
186                                       order_by=assoc_entity.mapper.order_by)
187                })
188       
189            entity = self.entity
190            entity.mapper.add_property(
191                attr_name, 
192                sa.orm.relation(GenericAssoc, lazy=self.lazy,
193                                backref='_backref_%s' % entity.table.name)
194            )
195
196            # this is strange! self.name is both set via mapper synonym and
197            # the python property
198            entity.mapper.add_property(self.name, sa.orm.synonym(attr_name))
199
200            if self.uselist:
201                def get(self):
202                    if getattr(self, attr_name) is None:
203                        setattr(self, attr_name, 
204                                GenericAssoc(entity.table.name))
205                    return getattr(self, attr_name).targets
206                setattr(entity, self.name, property(get))
207            else:
208                # scalar based property decorator
209                def get(self):
210                    attr = getattr(self, attr_name)
211                    if attr is not None:
212                        return attr.targets[0]
213                    else:
214                        return None
215                def set(self, value):
216                    if getattr(self, attr_name) is None:
217                        setattr(self, attr_name, 
218                                GenericAssoc(entity.table.name))
219                    getattr(self, attr_name).targets = [value]
220                setattr(entity, self.name, property(get, set))
221
222            # add helper methods
223            def select_by(cls, **kwargs):
224                return cls.query.join([attr_name, 'targets']) \
225                                .filter_by(**kwargs).all()
226            setattr(entity, 'select_by_%s' % self.name, classmethod(select_by))
227           
228            def select(cls, *args, **kwargs):
229                return cls.query.join([attr_name, 'targets']) \
230                                .filter(*args, **kwargs).all()
231            setattr(entity, 'select_%s' % self.name, classmethod(select))
232
233    return Statement(Associable)
Note: See TracBrowser for help on using the browser.