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

Revision 64, 23.7 kB (checked in by ged, 6 years ago)

- implemented singletable non-polymorphic inheritance
- added support for the nullable and column_kwargs kwargs on BelongsTo

relationships (forwarded to the SA Column).

- added support for the use_alter and constraint_kwargs kwargs on BelongsTo

relationships (forwarded to SA ForeignKeyConstraint).

-> removed the systematic use_alter on BelongsTo relations since it

can now be specified only when needed.

-> removed it from HasAndBelongsToMany relations, since I think a

circular foreign key dependency can't happen with those relations.

- corrected some docstrings
- added some comments

Line 
1'''
2Relationship statements for Elixir entities
3
4=============
5Relationships
6=============
7
8This module provides support for defining relationships between your Elixir
9entities.  Elixir supports the following types of relationships: belongs_to_,
10has_one_, has_many_ and has_and_belongs_to_many_.
11
12The first argument to all those statements is the name of the relationship, the
13second is the 'kind' of object you are relating to (it is usually given using
14the ``of_kind`` keyword).
15
16Additionally, if you want a bidirectionnal relationship, you should define the
17inverse relationship on the other entity explicitly (as opposed to how
18SQLAlchemy's backrefs are defined). In non-ambiguous situations, Elixir will
19match relationships together automatically. If there are several relationships
20of the same type between two entities, Elixir is not able to determine which
21relationship is the inverse of which, so you have to disambiguate the
22situation by giving the name of the inverse relationship in the ``inverse``
23keyword argument.
24
25Following these "common" arguments, any number of additional keyword arguments
26can be specified for advanced behavior. The keyword arguments are passed on to
27the SQLAlchemy ``relation`` function. Please refer to the `SQLAlchemy relation
28function's documentation <http://www.sqlalchemy.org/docs/adv_datamapping.myt
29#advdatamapping_properties_relationoptions>`_ for further detail about which
30keyword arguments are supported, but you should keep in mind, the following
31keyword arguments are taken care of by Elixir and should not be used:
32``uselist``, ``remote_side``, ``secondary``, ``primaryjoin`` and
33``secondaryjoin``.
34
35.. _order_by:
36
37Also, as for standard SQLAlchemy relations, the ``order_by`` keyword argument
38can be used to sort the results given by accessing a relation field (this only
39makes sense for has_many and has_and_belongs_to_many relationships). The value
40of that argument is different though: you can either use a string or a list of
41strings, each corresponding to the name of a field in the target entity. These
42field names can optionally be prefixed by a minus (for descending order).
43
44Here is a detailed explanation of each relation type:
45
46`belongs_to`
47------------
48
49Describes the child's side of a parent-child relationship.  For example,
50a `Pet` object may belong to its owner, who is a `Person`.  This could be
51expressed like so:
52
53::
54
55    class Pet(Entity):
56        belongs_to('owner', of_kind='Person')
57
58Behind the scene, assuming the primary key of the `Person` entity is
59an integer column named `id`, the ``belongs_to`` relationship will
60automatically add an integer column named `owner_id` to the entity, with a
61foreign key referencing the `id` column of the `Person` entity.
62
63In addition to the keyword arguments inherited from SQLAlchemy's relation
64function, ``belongs_to`` relationships accept the following optional arguments
65which will be directed to the created column:
66
67+----------------------+------------------------------------------------------+
68| Option Name          | Description                                          |
69+======================+======================================================+
70| ``colname``          | Specify a custom column name.                        |
71+----------------------+------------------------------------------------------+
72| ``nullable``         | True if this column should allow nulls. Defaults to  |
73|                      | True unless this column is a primary key column.     |
74+----------------------+------------------------------------------------------+
75| ``column_kwargs``    | A dictionary holding any other keyword argument you  |
76|                      | might want to pass to the Column.                    |
77+----------------------+------------------------------------------------------+
78
79The following optional arguments are also supported to customize the
80ForeignKeyConstraint that is created:
81
82+----------------------+------------------------------------------------------+
83| Option Name          | Description                                          |
84+======================+======================================================+
85| ``use_alter``        | If True, SQLAlchemy will add the constraint in a     |
86|                      | second SQL statement (as opposed to within the       |
87|                      | create table statement). This permits to define      |
88|                      | tables with a circular foreign key dependency        |
89|                      | between them.                                        |
90+----------------------+------------------------------------------------------+
91| ``constraint_kwargs``| A dictionary holding any other keyword argument you  |
92|                      | might want to pass to the Constraint.                |
93+----------------------+------------------------------------------------------+
94
95`has_one`
96---------
97
98Describes the parent's side of a parent-child relationship when there is only
99one child.  For example, a `Car` object has one gear stick, which is
100represented as a `GearStick` object. This could be expressed like so:
101
102::
103
104    class Car(Entity):
105        has_one('gear_stick', of_kind='GearStick', inverse='car')
106
107    class GearStick(Entity):
108        belongs_to('car', of_kind='Car')
109
110Note that an ``has_one`` relationship **cannot exist** without a corresponding
111``belongs_to`` relationship in the other way. This is because the ``has_one``
112relationship needs the foreign_key created by the ``belongs_to`` relationship.
113
114`has_many`
115----------
116
117Describes the parent's side of a parent-child relationship when there can be
118several children.  For example, a `Person` object has many children, each of
119them being a `Person`. This could be expressed like so:
120
121::
122
123    class Person(Entity):
124        belongs_to('parent', of_kind='Person')
125        has_many('children', of_kind='Person')
126
127Note that an ``has_many`` relationship **cannot exist** without a
128corresponding ``belongs_to`` relationship in the other way. This is because the
129``has_one`` relationship needs the foreign key created by the ``belongs_to``
130relationship.
131
132`has_and_belongs_to_many`
133-------------------------
134
135Describes a relationship in which one kind of entity can be related to several
136objects of the other kind but the objects of that other kind can be related to
137several objects of the first kind.  For example, an `Article` can have several
138tags, but the same `Tag` can be used on several articles.
139
140::
141
142    class Article(Entity):
143        has_and_belongs_to_many('tags', of_kind='Tag')
144
145    class Tag(Entity):
146        has_and_belongs_to_many('articles', of_kind='Article')
147
148Behind the scene, the ``has_and_belongs_to_many`` relationship will
149automatically create an intermediate table to host its data.
150
151Note that you don't necessarily need to define the inverse relationship.  In
152our example, even though we want tags to be usable on several articles, we
153might not be interested in which articles correspond to a particular tag.  In
154that case, we could have omitted the `Tag` side of the relationship.
155
156In addition to the order_by_ keyword argument, and the other keyword arguments
157inherited from SQLAlchemy, ``has_and_belongs_to_many`` relationships accept an
158optional ``tablename`` keyword argument, used to specify a custom name for the
159intermediary table which will be created.
160
161'''
162
163from sqlalchemy         import relation, ForeignKeyConstraint, Column, \
164                               Table, and_
165from elixir.statements  import Statement
166from elixir.fields      import Field
167from elixir.entity      import EntityDescriptor
168
169import sys
170
171
172__all__ = ['belongs_to', 'has_one', 'has_many', 'has_and_belongs_to_many']
173
174__pudge_all__ = []
175
176class Relationship(object):
177    '''
178    Base class for relationships.
179    '''
180   
181    def __init__(self, entity, name, *args, **kwargs):
182        self.name = name
183        self.of_kind = kwargs.pop('of_kind')
184        self.inverse_name = kwargs.pop('inverse', None)
185       
186        self.entity = entity
187        self._target = None
188       
189        self._inverse = None
190        self.foreign_key = kwargs.pop('foreign_key', None)
191        if self.foreign_key and not isinstance(self.foreign_key, list):
192            self.foreign_key = [self.foreign_key]
193       
194        self.property = None # sqlalchemy property
195       
196        #TODO: unused for now
197        self.args = args
198        self.kwargs = kwargs
199       
200        self.entity._descriptor.relationships[self.name] = self
201   
202    def create_keys(self):
203        '''
204        Subclasses (ie. concrete relationships) may override this method to
205        create foreign keys.
206        '''
207   
208    def create_tables(self):
209        '''
210        Subclasses (ie. concrete relationships) may override this method to
211        create secondary tables.
212        '''
213   
214    def create_properties(self):
215        '''
216        Subclasses (ie. concrete relationships) may override this method to add
217        properties to the involved entities.
218        '''
219   
220    def setup(self):
221        '''
222        Sets up the relationship, creates foreign keys and secondary tables.
223        '''
224       
225        if not self.target:
226            return False
227       
228        if self.property:
229            return True
230
231        self.create_keys()
232        self.create_tables()
233        self.create_properties()
234       
235        return True
236   
237    @property
238    def target(self):
239        if not self._target:
240            path = self.of_kind.rsplit('.', 1)
241            classname = path.pop()
242           
243            if path:
244                # do we have a fully qualified entity name?
245                module = sys.modules[path.pop()]
246            else: 
247                # if not, try the same module as the source
248                module = self.entity._descriptor.module
249           
250            self._target = getattr(module, classname, None)
251            if not self._target:
252                # This is ugly but we need it because the class which is
253                # currently being defined (we have to keep in mind we are in
254                # its metaclass code) is not yet available in the module
255                # namespace, so the getattr above fails. And unfortunately,
256                # this doesn't only happen for the owning entity of this
257                # relation since we might be setting up a deferred relation.
258                e = EntityDescriptor.current.entity
259                if classname == e.__name__ or \
260                        self.of_kind == e.__module__ +'.'+ e.__name__:
261                    self._target = e
262                else:
263                    return None
264       
265        return self._target
266   
267    @property
268    def inverse(self):
269        #TODO: we should use a different value for when an inverse was searched
270        # for but none was found than when it hasn't been searched for yet so
271        # that we don't do the whole search again
272        if not self._inverse:
273            if self.inverse_name:
274                desc = self.target._descriptor
275                inverse = desc.relationships[self.inverse_name]
276                assert self.match_type_of(inverse)
277            else:
278                inverse = self.target._descriptor.get_inverse_relation(self)
279       
280            if inverse:
281                self._inverse = inverse
282                inverse._inverse = self
283       
284        return self._inverse
285   
286    def match_type_of(self, other):
287        t1, t2 = type(self), type(other)
288   
289        if t1 is HasAndBelongsToMany:
290            return t1 is t2
291        elif t1 in (HasOne, HasMany):
292            return t2 is BelongsTo
293        elif t1 is BelongsTo:
294            return t2 in (HasMany, HasOne)
295        else:
296            return False
297
298    def is_inverse(self, other):
299        return other is not self and \
300               self.match_type_of(other) and \
301               self.entity == other.target and \
302               other.entity == self.target and \
303               (self.inverse_name == other.name or not self.inverse_name) and \
304               (other.inverse_name == self.name or not other.inverse_name)
305
306
307class BelongsTo(Relationship):
308    '''
309   
310    '''
311   
312    def __init__(self, entity, name, *args, **kwargs):
313        self.colname = kwargs.pop('colname', None)
314        self.column_kwargs = kwargs.pop('column_kwargs', {})
315        if 'nullable' in kwargs:
316            self.column_kwargs['nullable'] = kwargs.pop('nullable')
317
318        self.constraint_kwargs = kwargs.pop('constraint_kwargs', {})
319        if 'use_alter' in kwargs:
320            self.contraint_kwargs['use_alter'] = kwargs.pop('use_alter')
321       
322        if self.colname and not isinstance(self.colname, list):
323            self.colname = [self.colname]
324        super(BelongsTo, self).__init__(entity, name, *args, **kwargs)
325   
326    def create_keys(self):
327        '''
328        Find all primary keys on the target and create foreign keys on the
329        source accordingly.
330        '''
331       
332        source_desc = self.entity._descriptor
333        target_desc = self.target._descriptor
334       
335        # convert strings to column instances
336        if self.foreign_key:
337            #FIXME: this will fail. Because if we specify a foreign_key
338            # as argument, it will not create the necessary column
339            self.foreign_key = [source_desc.fields[k].column
340                                   for k in self.foreign_key 
341                                       if isinstance(k, basestring)]
342            return
343
344        self.foreign_key = list()
345        self.primaryjoin_clauses = list()
346
347        if source_desc.autoload:
348            if not self.colname:
349                raise Exception(
350                        "Entity '%s' is autoloaded but relation '%s' has no "
351                        "column name specified. You should specify it by "
352                        "using the colname keyword."
353                        % (self.entity.__name__, self.name)
354                      )
355
356            #TODO: test if this works when colname is a list
357            for colname in self.colname:
358                for col in self.entity.table.columns:
359                    if col.name == colname:
360                        # We need to take the first foreign key, but
361                        # foreign_keys is an util.OrderedSet which doesn't
362                        # support indexation.
363                        fk_iter = iter(col.foreign_keys)
364                        fk = fk_iter.next()
365                        self.primaryjoin_clauses.append(col == fk.column)
366
367            if not self.primaryjoin_clauses:
368                raise Exception("Column '%s' not found in table '%s'" 
369                                % (self.colname, self.entity.table.name))
370        else:
371            fk_refcols = list()
372            fk_colnames = list()
373
374            if self.colname and \
375               len(self.colname) != len(target_desc.primary_keys):
376                raise Exception(
377                        "The number of column names provided in the colname "
378                        "keyword argument of the '%s' relationship of the "
379                        "'%s' entity is not the same as the number of columns "
380                        "of the primary key of '%s'."
381                        % (self.name, self.entity.__name__, 
382                           self.target.__name__)
383                      )
384
385            for key_num, key in enumerate(target_desc.primary_keys):
386                pk_col = key.column
387
388                if self.colname:
389                    colname = self.colname[key_num]
390                else:
391                    colname = '%s_%s' % (self.name, pk_col.name)
392
393                # we use a Field here instead of using a Column directly
394                # because of add_field
395                field = Field(pk_col.type, colname=colname, index=True, 
396                              **self.column_kwargs)
397                source_desc.add_field(field)
398
399                self.foreign_key.append(field.column)
400
401                # build the list of local columns which will be part of
402                # the foreign key
403                fk_colnames.append(colname)
404
405                # build the list of columns the foreign key will point to
406                fk_refcols.append("%s.%s" % (target_desc.entity.table.name,
407                                             pk_col.name))
408
409                # build up the primary join. This is needed when you have
410                # several belongs_to relations between two objects
411                self.primaryjoin_clauses.append(field.column == pk_col)
412           
413            # TODO: better constraint-naming?
414            source_desc.add_constraint(ForeignKeyConstraint(
415                                            fk_colnames, fk_refcols,
416                                            name=self.name +'_fk',
417                                            **self.constraint_kwargs))
418   
419    def create_properties(self):
420        kwargs = self.kwargs
421       
422        if self.entity.table is self.target.table:
423            if self.entity._descriptor.autoload:
424                cols = [col for col in self.target.table.primary_key.columns]
425            else:
426                cols = [k.column for k in self.target._descriptor.primary_keys]
427            kwargs['remote_side'] = cols
428
429        kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
430        kwargs['uselist'] = False
431       
432        self.property = relation(self.target, **kwargs)
433        self.entity.mapper.add_property(self.name, self.property)
434
435
436class HasOne(Relationship):
437    uselist = False
438
439    def create_keys(self):
440        # make sure the inverse is set up because it creates the
441        # foreign key we'll need
442        self.inverse.setup()
443   
444    def create_properties(self):
445        kwargs = self.kwargs
446       
447        #TODO: for now, we don't break any test if we remove those 2 lines.
448        # So, we should either complete the selfref test to prove that they
449        # are indeed useful, or remove them. It might be they are indeed
450        # useless because of the primaryjoin, and that the remote_side is
451        # already setup in the other way (belongs_to).
452        if self.entity is self.target:
453            kwargs['remote_side'] = self.inverse.foreign_key
454            print "kwargs", self.name, kwargs
455       
456        kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses)
457        kwargs['uselist'] = self.uselist
458       
459        self.property = relation(self.target, **kwargs)
460        self.entity.mapper.add_property(self.name, self.property)
461
462
463class HasMany(HasOne):
464    uselist = True
465
466    def create_properties(self):
467        if 'order_by' in self.kwargs:
468            self.kwargs['order_by'] = \
469                self.target._descriptor.translate_order_by(
470                    self.kwargs['order_by'])
471
472        super(HasMany, self).create_properties()
473
474
475class HasAndBelongsToMany(Relationship):
476    def __init__(self, entity, name, *args, **kwargs):
477        self.user_tablename = kwargs.pop('tablename', None)
478        self.secondary_table = None
479        super(HasAndBelongsToMany, self).__init__(entity, name, 
480                                                  *args, **kwargs)
481
482    def create_tables(self):
483        if self.inverse:
484            if self.inverse.secondary_table:
485                self.secondary_table = self.inverse.secondary_table
486                self.primaryjoin_clauses = self.inverse.secondaryjoin_clauses
487                self.secondaryjoin_clauses = self.inverse.primaryjoin_clauses
488
489        if not self.secondary_table:
490            e1_desc = self.entity._descriptor
491            e2_desc = self.target._descriptor
492           
493            if e1_desc.autoload:
494                if not self.user_tablename:
495                    raise Exception(
496                        "Entity '%s' is autoloaded but relation '%s' has no "
497                        "secondary table name specified. You should specify "
498                        "it by using the tablename keyword."
499                        % (self.entity.__name__, self.name)
500                    )
501            columns = list()
502            constraints = list()
503
504            self.primaryjoin_clauses = list()
505            self.secondaryjoin_clauses = list()
506
507            for num, desc, join_name in (('1', e1_desc, 'primary'), 
508                                         ('2', e2_desc, 'secondary')):
509                fk_colnames = list()
510                fk_refcols = list()
511           
512                for key in desc.primary_keys:
513                    pk_col = key.column
514                   
515                    colname = '%s_%s' % (desc.tablename, pk_col.name)
516
517                    # In case we have a many-to-many self-reference, we need
518                    # to tweak the names of the columns so that we don't end
519                    # up with twice the same column name.
520                    if self.entity is self.target:
521                        colname += num
522
523                    col = Column(colname, pk_col.type)
524                    columns.append(col)
525
526                    # build the list of local columns which will be part of
527                    # the foreign key
528                    fk_colnames.append(colname)
529
530                    # build the list of columns the foreign key will point to
531                    fk_refcols.append(desc.tablename + '.' + pk_col.name)
532
533                    # build join clauses
534                    join_list = getattr(self, join_name+'join_clauses')
535                    join_list.append(col == pk_col)
536               
537                # TODO: better constraint-naming?
538                constraints.append(
539                    ForeignKeyConstraint(fk_colnames, fk_refcols,
540                                         name=desc.tablename + '_fk'))
541
542            if self.user_tablename:
543                tablename = self.user_tablename
544            else:
545                # We use the name of the relation for the first entity
546                # (instead of the name of its primary key), so that we can
547                # have two many-to-many relations between the same objects
548                # without having a table name collision.
549                source_part = "%s_%s" % (e1_desc.tablename, self.name)
550
551                # And we use the name of the primary key for the second entity
552                # when there is no inverse, so that a many-to-many relation
553                # can be defined without an inverse.
554                if self.inverse:
555                    e2_name = self.inverse.name
556                else:
557                    e2_name = '_'.join([key.column.name for key in
558                                        e2_desc.primary_keys])
559                target_part = "%s_%s" % (e2_desc.tablename, e2_name)
560
561                # we need to keep the table name consistent (independant of
562                # whether this relation or its inverse is setup first)
563                if self.inverse and e1_desc.tablename < e2_desc.tablename:
564                    tablename = "%s__%s" % (target_part, source_part)
565                else:
566                    tablename = "%s__%s" % (source_part, target_part)
567
568            args = columns + constraints
569           
570            self.secondary_table = Table(tablename, e1_desc.metadata, *args)
571   
572    def create_properties(self):
573        kwargs = self.kwargs
574
575        if self.target is self.entity:
576            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
577            kwargs['secondaryjoin'] = and_(*self.secondaryjoin_clauses)
578
579        if 'order_by' in kwargs:
580            kwargs['order_by'] = \
581                self.target._descriptor.translate_order_by(kwargs['order_by'])
582
583        self.property = relation(self.target, secondary=self.secondary_table,
584                                 uselist=True, **kwargs)
585        self.entity.mapper.add_property(self.name, self.property)
586
587    def is_inverse(self, other):
588        return super(HasAndBelongsToMany, self).is_inverse(other) and \
589               (self.user_tablename == other.user_tablename or 
590                (not self.user_tablename and not other.user_tablename))
591
592
593belongs_to              = Statement(BelongsTo)
594has_one                 = Statement(HasOne)
595has_many                = Statement(HasMany)
596has_and_belongs_to_many = Statement(HasAndBelongsToMany)
Note: See TracBrowser for help on using the browser.