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

Revision 172, 8.9 kB (checked in by cleverdevil, 6 years ago)

Added two new extensions to elixir:

  • elixir.ext.versioned - automatic versioning for elixir entities
  • elixir.ext.encrypted - configurable encryption for elixir entities

For more information, see the unit tests and docstrings. These are both a work in progress.

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            # CHANGE: I had to change the second argument from None to sa.Integer
171            # in order to get associable working with the versioning extension...
172            # Ben: was this the right thing to do?
173            field.column = sa.Column('%s_assoc_id' % interface_name, sa.Integer, 
174                                  sa.ForeignKey('%s.%s_id' % (able_name, able_name)))
175            entity._descriptor.add_field(field)
176            entity._descriptor.relationships[able_name] = self
177           
178            def select_by(cls, **kwargs):
179                return cls.query().join(attr_name).join('targets').filter_by(**kwargs).list()
180            setattr(entity, 'select_by_%s' % self.name, classmethod(select_by))
181           
182            def select(cls, *args, **kwargs):
183                return cls.query().join(attr_name).join('targets').filter(*args, **kwargs).list()
184            setattr(entity, 'select_%s' % self.name, classmethod(select))
185       
186        def setup(self):
187            self.create_properties()
188            return True
189       
190        def create_properties(self):
191            entity = self.entity
192            entity.mapper.add_property(attr_name, sa.relation(GenericAssoc, lazy=self.lazy,
193                                       backref='_backref_%s' % entity.table.name))
194            entity.mapper.add_property(self.name, sa.synonym(attr_name))
195            if self.uselist:
196                def get(self):
197                    if getattr(self, attr_name) is None:
198                        setattr(self, attr_name, 
199                                GenericAssoc(entity.table.name))
200                    return getattr(self, attr_name).targets
201                setattr(entity, self.name, property(get))
202            else:
203                # scalar based property decorator
204                def get(self):
205                    attr = getattr(self, attr_name)
206                    if attr is not None:
207                        return attr.targets[0]
208                    else:
209                        return None
210                def set(self, value):
211                    if getattr(self, attr_name) is None:
212                        setattr(self, attr_name, 
213                                GenericAssoc(entity.table.name))
214                    getattr(self, attr_name).targets = [value]
215                setattr(entity, self.name, property(get, set))
216
217    sa.mapper(GenericAssoc, association_table, properties={
218        'targets': sa.relation(entity, secondary=association_to_table,
219                               lazy=lazy, backref='association',
220                               order_by=entity.mapper.order_by)
221    })
222    return Statement(Associable)
Note: See TracBrowser for help on using the browser.