root / elixir / trunk / elixir / relationships.py @ 17

Revision 17, 11.9 kB (checked in by cleverdevil, 6 years ago)

Finishing the name change from 'supermodel' to 'elixir'.

Line 
1from sqlalchemy         import relation, ForeignKeyConstraint, Column, \
2                               Table, and_
3from elixir.statements  import Statement
4from elixir.fields      import Field
5from elixir.entity      import EntityDescriptor
6
7import sys
8
9
10__all__ = [
11    'belongs_to',
12    'has_one',
13    'has_many',
14    'has_and_belongs_to_many'
15]
16
17
18class Relationship(object):
19    '''
20    Base class for relationships
21    '''
22   
23    def __init__(self, entity, name, *args, **kwargs):
24        self.name = name
25        self.of_kind = kwargs.pop('of_kind')
26        self.inverse_name = kwargs.pop('inverse', None)
27       
28        self.entity = entity
29        self._target = None
30       
31        self.initialized = False
32        self.secondary = None
33        self._inverse = None
34        self.foreign_key = None
35       
36        self.foreign_key = kwargs.pop('foreign_key', None)
37        if self.foreign_key and not isinstance(self.foreign_key, list):
38            self.foreign_key = [self.foreign_key]
39       
40        self.property = None # sqlalchemy property
41       
42        self.args = args
43        self.kwargs = kwargs
44       
45        #CHECKME: is this useful?
46        self.entity._descriptor.relationships[self.name] = self
47   
48    def create_keys(self):
49        '''
50        Subclasses (ie. concrete relationships) may override this method to
51        create foreign keys.
52        '''
53   
54    def create_tables(self):
55        '''
56        Subclasses (ie. concrete relationships) may override this method to
57        create secondary tables.
58        '''
59   
60    def create_properties(self):
61        '''
62        Subclasses (ie. concrete relationships) may override this method to add
63        properties to the involved entities.
64        '''
65   
66    def setup(self):
67        '''
68        Sets up the relationship, creates foreign keys and secondary tables.
69        '''
70       
71        if not self.target:
72            return False
73       
74        self.create_keys()
75        self.create_tables()
76        self.create_properties()
77       
78        return True
79   
80    @property
81    def target(self):
82        if not self._target:
83            path = self.of_kind.rsplit('.', 1)
84            classname = path.pop()
85
86            # full qualified entity name?
87            if path:
88                module = sys.modules[path.pop()]
89            # if not, try the same module as the source
90            else: 
91                module = self.entity._descriptor.module
92           
93            try:
94                self._target = getattr(module, classname)
95            except AttributeError:
96                # TODO: don't use exceptions for logic here!
97                # This is ugly but we need it because the class which is
98                # currently being defined (we have to keep in mind we are in
99                # its metaclass code) is not yet available in the module
100                # namespace, so the getattr above fails. And unfortunately,
101                # this doesn't only happen for the owning entity of this
102                # relation since we might be setting up a deferred relation.
103                e = EntityDescriptor.current.entity
104                if classname == e.__name__ or \
105                        self.of_kind == e.__module__ +'.'+ e.__name__:
106                    self._target = e
107                else:
108                    return None
109       
110        return self._target
111   
112    @property
113    def inverse(self):
114        #TODO: we should use a different value for when an inverse was searched
115        # for but none was found than when it hasn't been searched for yet so
116        # that we don't do the whole search again
117        if not self._inverse:
118            if self.inverse_name:
119                desc = self.target._descriptor
120                inverse = desc.relationships[self.inverse_name]
121                assert self.match_type_of(inverse)
122            else:
123                inverse = self.target._descriptor.get_inverse_relation(self)
124       
125            if inverse:
126                self._inverse = inverse
127                inverse._inverse = self
128       
129        return self._inverse
130   
131    def match_type_of(self, other):
132        t1, t2 = type(self), type(other)
133   
134        if t1 is HasAndBelongsToMany:
135            return t1 is t2
136        elif t1 in (HasOne, HasMany):
137            return t2 is BelongsTo
138        elif t1 is BelongsTo:
139            return t2 in (HasMany, HasOne)
140        else:
141            return False
142
143    def is_inverse(self, other):
144        return other is not self and \
145               self.match_type_of(other) and \
146               self.entity == other.target and \
147               other.entity == self.target and \
148               (self.inverse_name == other.name or not self.inverse_name) and \
149               (other.inverse_name == self.name or not other.inverse_name)
150
151
152class BelongsTo(Relationship):
153   
154    def create_keys(self):
155        '''
156        Find all primary keys on the target and create foreign keys on the
157        source accordingly.
158        '''
159       
160        source_desc = self.entity._descriptor
161        target_desc = self.target._descriptor
162       
163        if self.foreign_key:
164            self.foreign_key = [source_desc.fields[k]
165                                    for k in self.foreign_key 
166                                        if isinstance(k, basestring)]
167            return
168       
169        fk_refcols = list()
170        fk_colnames = list()
171
172        self.foreign_key = list()
173        self.primaryjoin_clauses = list()
174
175        for key in target_desc.primary_keys:
176            pk_col = key.column
177
178            colname = '%s_%s' % (self.name, pk_col.name)
179            # we use a Field here instead of using a Column directly
180            # because of add_field
181            field = Field(pk_col.type, colname=colname, index=True)
182            source_desc.add_field(field)
183
184            self.foreign_key.append(field)
185
186            # build the list of local columns which will be part of
187            # the foreign key
188            fk_colnames.append(colname)
189
190            # build the list of columns the foreign key will point to
191            fk_refcols.append(target_desc.tablename + '.' + pk_col.name)
192
193            # build up the primary join. This is needed when you have several
194            # belongs_to relations between two objects
195            self.primaryjoin_clauses.append(field.column == pk_col)
196       
197        # TODO: better constraint-naming?
198        #CHECKME: do we really need use_alter systematically?
199        source_desc.add_constraint(ForeignKeyConstraint(
200                                        fk_colnames, fk_refcols,
201                                        name=self.name +'_fk',
202                                        use_alter=True))
203   
204    def create_properties(self):
205        kwargs = self.kwargs
206       
207        if self.entity is self.target:
208            cols = [k.column for k in self.target._descriptor.primary_keys]
209            kwargs['remote_side'] = cols
210
211        kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
212        kwargs['uselist'] = False
213       
214        self.property = relation(self.target, **kwargs)
215        self.entity.mapper.add_property(self.name, self.property)
216
217
218class HasOne(Relationship):
219    uselist = False
220
221    def create_keys(self):
222        # make sure the inverse is set up because it creates the
223        # foreign key we'll need
224        self.inverse.setup()
225   
226    def create_properties(self):
227        kwargs = self.kwargs
228       
229        if self.entity is self.target:
230            kwargs['remote_side'] = [field.column
231                                        for field in self.inverse.foreign_key]
232       
233        kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses)
234        kwargs['uselist'] = self.uselist
235       
236        self.property = relation(self.target, **kwargs)
237        self.entity.mapper.add_property(self.name, self.property)
238
239
240class HasMany(HasOne):
241    uselist = True
242
243
244class HasAndBelongsToMany(Relationship):
245   
246    def __init__(self, entity, name, *args, **kwargs):
247        self.tablename = kwargs.pop('tablename', None)
248        super(HasAndBelongsToMany, self).__init__(entity, name, *args, **kwargs)
249   
250    def create_tables(self):
251        if self.inverse:
252            if self.inverse.secondary:
253                self.secondary = self.inverse.secondary
254                self.primaryjoin_clauses = self.inverse.secondaryjoin_clauses
255                self.secondaryjoin_clauses = self.inverse.primaryjoin_clauses
256
257        if not self.secondary:
258            e1_desc = self.entity._descriptor
259            e2_desc = self.target._descriptor
260           
261            columns = list()
262            constraints = list()
263
264            self.primaryjoin_clauses = list()
265            self.secondaryjoin_clauses = list()
266
267            for num, desc, join_name in (('1', e1_desc, 'primary'), 
268                                         ('2', e2_desc, 'secondary')):
269                fk_colnames = list()
270                fk_refcols = list()
271           
272                for key in desc.primary_keys:
273                    pk_col = key.column
274                   
275                    colname = '%s_%s' % (desc.tablename, pk_col.name)
276
277                    # In case we have a many-to-many self-reference, we need
278                    # to tweak the names of the columns so that we don't end
279                    # up with twice the same column name.
280                    if self.entity is self.target:
281                        colname += num
282
283                    col = Column(colname, pk_col.type)
284                    columns.append(col)
285
286                    # build the list of local columns which will be part of
287                    # the foreign key
288                    fk_colnames.append(colname)
289
290                    # build the list of columns the foreign key will point to
291                    fk_refcols.append(desc.tablename + '.' + pk_col.name)
292
293                    # build join clauses
294                    join_list = getattr(self, join_name+'join_clauses')
295                    join_list.append(col == pk_col)
296               
297                # TODO: better constraint-naming?
298                #CHECKME: do we really need use_alter systematically?
299                constraints.append(
300                    ForeignKeyConstraint(fk_colnames, fk_refcols,
301                                         name=desc.tablename + '_fk', 
302                                         use_alter=True))
303       
304            # In the table name code below, we use the name of the relation
305            # for the first entity (instead of the name of its primary key),
306            # so that we can have two many-to-many relations between the same
307            # objects without having a table name collision. On the other hand,
308            # we use the name of the primary key for the second entity
309            # (instead of the inverse relation's name) so that a many-to-many
310            # relation can be defined without inverse.
311            if not self.tablename:
312                e2_pk_name = '_'.join([key.column.name for key in
313                                       e2_desc.primary_keys])
314                tablename = "%s_%s__%s_%s" % (e1_desc.tablename, self.name,
315                                              e2_desc.tablename, e2_pk_name)
316            else:
317                tablename = self.tablename
318
319            args = columns + constraints
320            self.secondary = Table(tablename, e1_desc.metadata, *args)
321   
322    def create_properties(self):
323        kwargs = self.kwargs
324
325        if self.target is self.entity:
326            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
327            kwargs['secondaryjoin'] = and_(*self.secondaryjoin_clauses)
328
329        m = self.entity.mapper
330        #FIXME: using post_update systematically is *really* not good
331        m.add_property(self.name,
332                       relation(self.target, secondary=self.secondary,
333                                uselist=True, **kwargs))
334
335
336belongs_to              = Statement(BelongsTo)
337has_one                 = Statement(HasOne)
338has_many                = Statement(HasMany)
339has_and_belongs_to_many = Statement(HasAndBelongsToMany)
Note: See TracBrowser for help on using the browser.