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

Revision 195, 9.0 kB (checked in by ged, 6 years ago)

small fix so that the column is not added twice

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(assoc_entity, plural_name=None, lazy=True):
117    '''
118    Generate an associable Elixir Statement
119    '''
120    interface_name = assoc_entity._descriptor.tablename
121    able_name = interface_name + 'able'
122
123    if plural_name:
124        attr_name = "%s_rel" % plural_name
125    else:
126        plural_name = interface_name
127        attr_name = "%s_rel" % interface_name
128
129    class GenericAssoc(object):
130        def __init__(self, tablename):
131            self.type = tablename
132   
133    class Associable(el.relationships.Relationship):
134        """An associable Elixir Statement object"""
135        def __init__(self, entity, name=None, uselist=True, lazy=True):
136            self.entity = entity
137            self.lazy = lazy
138            self.uselist = uselist
139           
140            if name is None:
141                self.name = plural_name
142            else:
143                self.name = name
144            self.entity._descriptor.relationships[able_name] = self
145
146        def create_keys(self, pk):
147            if pk:
148                return
149            field = el.Field(sa.Integer, sa.ForeignKey('%s.id' % able_name),
150                             colname='%s_assoc_id' % interface_name)
151            self.entity._descriptor.add_field(field)
152
153        def create_tables(self):
154            if not hasattr(assoc_entity, '_assoc_table'):
155                association_table = sa.Table("%s" % able_name, assoc_entity._descriptor.metadata,
156                    sa.Column('id', sa.Integer, primary_key=True),
157                    sa.Column('type', sa.String(40), nullable=False),
158                )
159               
160                association_to_table = sa.Table("%s_to_%s" % (able_name, interface_name), assoc_entity._descriptor.metadata,
161                    sa.Column('assoc_id', sa.Integer, sa.ForeignKey(association_table.c.id, ondelete="CASCADE"), primary_key=True),
162                    #FIXME: this assumes a single id col
163                    sa.Column('%s_id' % interface_name, sa.Integer, sa.ForeignKey(assoc_entity.table.c.id, ondelete="RESTRICT"), primary_key=True),
164                )
165
166                assoc_entity._assoc_table = association_table
167                assoc_entity._assoc_to_table = association_to_table
168
169        def after_mapper(self):
170            if not hasattr(assoc_entity, '_assoc_mapper'):
171                assoc_entity._assoc_mapper = sa.orm.mapper(
172                    GenericAssoc, assoc_entity._assoc_table, properties={
173                        'targets': sa.orm.relation(
174                                       assoc_entity,
175                                       secondary=assoc_entity._assoc_to_table,
176                                       lazy=lazy, backref='associations',
177                                       order_by=assoc_entity.mapper.order_by)
178                })
179       
180        def create_properties(self):
181            entity = self.entity
182            entity.mapper.add_property(
183                attr_name, 
184                sa.orm.relation(GenericAssoc, lazy=self.lazy,
185                                backref='_backref_%s' % entity.table.name)
186            )
187
188            # this is strange! self.name is both set via mapper synonym and
189            # the python property
190            entity.mapper.add_property(self.name, sa.orm.synonym(attr_name))
191
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            # add helper methods
215            def select_by(cls, **kwargs):
216                return cls.query().join([attr_name, 'targets']).filter_by(**kwargs).all()
217            setattr(entity, 'select_by_%s' % self.name, classmethod(select_by))
218           
219            def select(cls, *args, **kwargs):
220                return cls.query().join([attr_name, 'targets']).filter(*args, **kwargs).all()
221            setattr(entity, 'select_%s' % self.name, classmethod(select))
222
223    return Statement(Associable)
Note: See TracBrowser for help on using the browser.