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

Revision 84, 26.1 kB (checked in by ged, 6 years ago)

- Fixed documentation for belongs_to relationships (the arguemnt is "required",

not "nullable").

- Fixed typo which broke the use_alter argument on belongs_to relationships.
- reordered CHANGES file: feature changes/addition then bugfixes then cosmectic/cleanup

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| ``required``         | Specify whether or not this field can be set to None |
73|                      | (left without a value). Defaults to ``False``,       |
74|                      | unless the field is a primary key.                   |
75+----------------------+------------------------------------------------------+
76| ``column_kwargs``    | A dictionary holding any other keyword argument you  |
77|                      | might want to pass to the Column.                    |
78+----------------------+------------------------------------------------------+
79
80The following optional arguments are also supported to customize the
81ForeignKeyConstraint that is created:
82
83+----------------------+------------------------------------------------------+
84| Option Name          | Description                                          |
85+======================+======================================================+
86| ``use_alter``        | If True, SQLAlchemy will add the constraint in a     |
87|                      | second SQL statement (as opposed to within the       |
88|                      | create table statement). This permits to define      |
89|                      | tables with a circular foreign key dependency        |
90|                      | between them.                                        |
91+----------------------+------------------------------------------------------+
92| ``constraint_kwargs``| A dictionary holding any other keyword argument you  |
93|                      | might want to pass to the Constraint.                |
94+----------------------+------------------------------------------------------+
95
96`has_one`
97---------
98
99Describes the parent's side of a parent-child relationship when there is only
100one child.  For example, a `Car` object has one gear stick, which is
101represented as a `GearStick` object. This could be expressed like so:
102
103::
104
105    class Car(Entity):
106        has_one('gear_stick', of_kind='GearStick', inverse='car')
107
108    class GearStick(Entity):
109        belongs_to('car', of_kind='Car')
110
111Note that an ``has_one`` relationship **cannot exist** without a corresponding
112``belongs_to`` relationship in the other way. This is because the ``has_one``
113relationship needs the foreign_key created by the ``belongs_to`` relationship.
114
115`has_many`
116----------
117
118Describes the parent's side of a parent-child relationship when there can be
119several children.  For example, a `Person` object has many children, each of
120them being a `Person`. This could be expressed like so:
121
122::
123
124    class Person(Entity):
125        belongs_to('parent', of_kind='Person')
126        has_many('children', of_kind='Person')
127
128Note that an ``has_many`` relationship **cannot exist** without a
129corresponding ``belongs_to`` relationship in the other way. This is because the
130``has_many`` relationship needs the foreign key created by the ``belongs_to``
131relationship.
132
133`has_and_belongs_to_many`
134-------------------------
135
136Describes a relationship in which one kind of entity can be related to several
137objects of the other kind but the objects of that other kind can be related to
138several objects of the first kind.  For example, an `Article` can have several
139tags, but the same `Tag` can be used on several articles.
140
141::
142
143    class Article(Entity):
144        has_and_belongs_to_many('tags', of_kind='Tag')
145
146    class Tag(Entity):
147        has_and_belongs_to_many('articles', of_kind='Article')
148
149Behind the scene, the ``has_and_belongs_to_many`` relationship will
150automatically create an intermediate table to host its data.
151
152Note that you don't necessarily need to define the inverse relationship.  In
153our example, even though we want tags to be usable on several articles, we
154might not be interested in which articles correspond to a particular tag.  In
155that case, we could have omitted the `Tag` side of the relationship.
156
157In addition to the order_by_ keyword argument, and the other keyword arguments
158inherited from SQLAlchemy, ``has_and_belongs_to_many`` relationships accept an
159optional ``tablename`` keyword argument, used to specify a custom name for the
160intermediary table which will be created.
161
162'''
163
164from sqlalchemy         import relation, ForeignKeyConstraint, Column, \
165                               Table, and_
166from elixir.statements  import Statement
167from elixir.fields      import Field
168from elixir.entity      import EntityDescriptor
169
170import sys
171
172
173__all__ = ['belongs_to', 'has_one', 'has_many', 'has_and_belongs_to_many']
174
175__pudge_all__ = []
176
177class Relationship(object):
178    '''
179    Base class for relationships.
180    '''
181   
182    def __init__(self, entity, name, *args, **kwargs):
183        self.entity = entity
184        self.name = name
185        self.of_kind = kwargs.pop('of_kind')
186        self.inverse_name = kwargs.pop('inverse', None)
187       
188        self._target = None
189        self._inverse = None
190       
191        self.property = None # sqlalchemy property
192       
193        #TODO: unused for now
194        self.args = args
195        self.kwargs = kwargs
196       
197        self.entity._descriptor.relationships[self.name] = self
198   
199    def create_keys(self):
200        '''
201        Subclasses (ie. concrete relationships) may override this method to
202        create foreign keys.
203        '''
204   
205    def create_tables(self):
206        '''
207        Subclasses (ie. concrete relationships) may override this method to
208        create secondary tables.
209        '''
210   
211    def create_properties(self):
212        '''
213        Subclasses (ie. concrete relationships) may override this method to add
214        properties to the involved entities.
215        '''
216   
217    def setup(self):
218        '''
219        Sets up the relationship, creates foreign keys and secondary tables.
220        '''
221
222        if not self.target:
223            return False
224
225        if self.property:
226            return True
227
228        self.create_keys()
229        self.create_tables()
230        self.create_properties()
231       
232        return True
233   
234    def target(self):
235        if not self._target:
236            path = self.of_kind.rsplit('.', 1)
237            classname = path.pop()
238
239            if path:
240                # do we have a fully qualified entity name?
241                module = sys.modules[path.pop()]
242            else: 
243                # if not, try the same module as the source
244                module = self.entity._descriptor.module
245
246            self._target = getattr(module, classname, None)
247            if not self._target:
248                # This is ugly but we need it because the class which is
249                # currently being defined (we have to keep in mind we are in
250                # its metaclass code) is not yet available in the module
251                # namespace, so the getattr above fails. And unfortunately,
252                # this doesn't only happen for the owning entity of this
253                # relation since we might be setting up a deferred relation.
254                e = EntityDescriptor.current.entity
255                if classname == e.__name__ or \
256                        self.of_kind == e.__module__ +'.'+ e.__name__:
257                    self._target = e
258                else:
259                    return None
260       
261        return self._target
262    target = property(target)
263   
264    def inverse(self):
265        if not self._inverse:
266            if self.inverse_name:
267                desc = self.target._descriptor
268                # we use all_relationships so that relationships from parent
269                # entities are included too
270                inverse = desc.all_relationships.get(self.inverse_name, None)
271                if inverse is None:
272                    raise Exception(
273                              "Couldn't find a relationship named '%s' in "
274                              "entity '%s' or its parent entities." 
275                              % (self.inverse_name, self.target.__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    inverse = property(inverse)
286   
287    def match_type_of(self, other):
288        t1, t2 = type(self), type(other)
289   
290        if t1 is HasAndBelongsToMany:
291            return t1 is t2
292        elif t1 in (HasOne, HasMany):
293            return t2 is BelongsTo
294        elif t1 is BelongsTo:
295            return t2 in (HasMany, HasOne)
296        else:
297            return False
298
299    def is_inverse(self, other):
300        return other is not self and \
301               self.match_type_of(other) and \
302               self.entity == other.target and \
303               other.entity == self.target and \
304               (self.inverse_name == other.name or not self.inverse_name) and \
305               (other.inverse_name == self.name or not other.inverse_name)
306
307
308class BelongsTo(Relationship):
309    '''
310   
311    '''
312   
313    def __init__(self, entity, name, *args, **kwargs):
314        self.colname = kwargs.pop('colname', [])
315        if self.colname and not isinstance(self.colname, list):
316            self.colname = [self.colname]
317
318        self.column_kwargs = kwargs.pop('column_kwargs', {})
319        if 'required' in kwargs:
320            self.column_kwargs['nullable'] = not kwargs.pop('required')
321
322        self.constraint_kwargs = kwargs.pop('constraint_kwargs', {})
323        if 'use_alter' in kwargs:
324            self.constraint_kwargs['use_alter'] = kwargs.pop('use_alter')
325
326        self.foreign_key = list()
327        self.primaryjoin_clauses = list()
328        super(BelongsTo, self).__init__(entity, name, *args, **kwargs)
329   
330    def create_keys(self):
331        '''
332        Find all primary keys on the target and create foreign keys on the
333        source accordingly.
334        '''
335
336        source_desc = self.entity._descriptor
337        target_desc = self.target._descriptor
338
339        if source_desc.autoload:
340
341            #TODO: test if this works when colname is a list
342            for colname in self.colname:
343                col_found = False
344                for col in self.entity.table.columns:
345                    if col.name == colname:
346                        # We need to take the first foreign key, but
347                        # foreign_keys is an util.OrderedSet which doesn't
348                        # support indexation.
349                        fk_iter = iter(col.foreign_keys)
350                        fk = fk_iter.next()
351                        self.primaryjoin_clauses.append(col == fk.column)
352                        col_found = True
353
354                if not col_found:
355                    raise Exception("Column '%s' not found in table '%s'" 
356                                    % (colname, self.entity.table.name))
357        else:
358            fk_refcols = list()
359            fk_colnames = list()
360
361            if self.colname and \
362               len(self.colname) != len(target_desc.primary_keys):
363                raise Exception(
364                        "The number of column names provided in the colname "
365                        "keyword argument of the '%s' relationship of the "
366                        "'%s' entity is not the same as the number of columns "
367                        "of the primary key of '%s'."
368                        % (self.name, self.entity.__name__, 
369                           self.target.__name__))
370
371            for key_num, key in enumerate(target_desc.primary_keys):
372                pk_col = key.column
373
374                if self.colname:
375                    colname = self.colname[key_num]
376                else:
377                    colname = '%s_%s' % (self.name, pk_col.name)
378
379                # we use a Field here instead of using a Column directly
380                # because of add_field
381                field = Field(pk_col.type, colname=colname, index=True, 
382                              **self.column_kwargs)
383                source_desc.add_field(field)
384
385                # build the list of local columns which will be part of
386                # the foreign key
387                self.foreign_key.append(field.column)
388
389                # store the names of those columns
390                fk_colnames.append(colname)
391
392                # build the list of columns the foreign key will point to
393                fk_refcols.append("%s.%s" % (target_desc.entity.table.name,
394                                             pk_col.name))
395
396                # build up the primary join. This is needed when you have
397                # several belongs_to relations between two objects
398                self.primaryjoin_clauses.append(field.column == pk_col)
399           
400            # In some databases (at lease MySQL) the constraint name needs to
401            # be unique for the whole database, instead of per table.
402            fk_name = "%s_%s_fk" % (self.entity.table.name, 
403                                    '_'.join(fk_colnames))
404            source_desc.add_constraint(ForeignKeyConstraint(
405                                            fk_colnames, fk_refcols,
406                                            name=fk_name,
407                                            **self.constraint_kwargs))
408   
409    def create_properties(self):
410        kwargs = self.kwargs
411       
412        if self.entity.table is self.target.table:
413            if self.entity._descriptor.autoload:
414                cols = [col for col in self.target.table.primary_key.columns]
415            else:
416                cols = [k.column for k in self.target._descriptor.primary_keys]
417            kwargs['remote_side'] = cols
418
419        if self.primaryjoin_clauses:
420            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
421        kwargs['uselist'] = False
422       
423        self.property = relation(self.target, **kwargs)
424        self.entity.mapper.add_property(self.name, self.property)
425
426
427class HasOne(Relationship):
428    uselist = False
429
430    def create_keys(self):
431        # make sure an inverse relationship exists
432        if self.inverse is None:
433            raise Exception(
434                      "Couldn't find any relationship in '%s' which "
435                      "match as inverse of the '%s' relationship "
436                      "defined in the '%s' entity. If you are using "
437                      "inheritance you "
438                      "might need to specify inverse relationships "
439                      "manually by using the inverse keyword."
440                      % (self.target.__name__, self.name,
441                         self.entity.__name__))
442        # make sure it is set up because it creates the foreign key we'll need
443        self.inverse.setup()
444   
445    def create_properties(self):
446        kwargs = self.kwargs
447       
448        #TODO: for now, we don't break any test if we remove those 2 lines.
449        # So, we should either complete the selfref test to prove that they
450        # are indeed useful, or remove them. It might be they are indeed
451        # useless because of the primaryjoin, and that the remote_side is
452        # already setup in the other way (belongs_to).
453        if self.entity.table is self.target.table:
454            #FIXME: IF this code is of any use, it will probably break for
455            # autoloaded tables
456            kwargs['remote_side'] = self.inverse.foreign_key
457       
458        if self.inverse.primaryjoin_clauses:
459            kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses)
460
461        kwargs['uselist'] = self.uselist
462       
463        self.property = relation(self.target, **kwargs)
464        self.entity.mapper.add_property(self.name, self.property)
465
466
467class HasMany(HasOne):
468    uselist = True
469
470    def create_properties(self):
471        if 'order_by' in self.kwargs:
472            self.kwargs['order_by'] = \
473                self.target._descriptor.translate_order_by(
474                    self.kwargs['order_by'])
475
476        super(HasMany, self).create_properties()
477
478
479class HasAndBelongsToMany(Relationship):
480
481    def __init__(self, entity, name, *args, **kwargs):
482        self.user_tablename = kwargs.pop('tablename', None)
483        self.secondary_table = None
484        self.primaryjoin_clauses = list()
485        self.secondaryjoin_clauses = list()
486        super(HasAndBelongsToMany, self).__init__(entity, name, 
487                                                  *args, **kwargs)
488
489    def create_tables(self):
490        if self.inverse:
491            if self.inverse.secondary_table:
492                self.secondary_table = self.inverse.secondary_table
493                self.primaryjoin_clauses = self.inverse.secondaryjoin_clauses
494                self.secondaryjoin_clauses = self.inverse.primaryjoin_clauses
495
496        if not self.secondary_table:
497            e1_desc = self.entity._descriptor
498            e2_desc = self.target._descriptor
499           
500            # First, we compute the name of the table. Note that some of the
501            # intermediary variables are reused later for the constraint
502            # names.
503           
504            # We use the name of the relation for the first entity
505            # (instead of the name of its primary key), so that we can
506            # have two many-to-many relations between the same objects
507            # without having a table name collision.
508            source_part = "%s_%s" % (e1_desc.tablename, self.name)
509
510            # And we use only the name of the table of the second entity
511            # when there is no inverse, so that a many-to-many relation
512            # can be defined without an inverse.
513            if self.inverse:
514                target_part = "%s_%s" % (e2_desc.tablename, self.inverse.name)
515            else:
516                target_part = e2_desc.tablename
517           
518            if self.user_tablename:
519                tablename = self.user_tablename
520            else:
521                # We need to keep the table name consistent (independant of
522                # whether this relation or its inverse is setup first).
523                if self.inverse and e1_desc.tablename < e2_desc.tablename:
524                    tablename = "%s__%s" % (target_part, source_part)
525                else:
526                    tablename = "%s__%s" % (source_part, target_part)
527
528            if e1_desc.autoload:
529                self._reflect_table(tablename)
530            else:
531                # We pre-compute the names of the foreign key constraints
532                # pointing to the source (local) entity's table and to the
533                # target's table
534
535                # In some databases (at lease MySQL) the constraint names need
536                # to be unique for the whole database, instead of per table.
537                source_fk_name = "%s_fk" % source_part
538                if self.inverse:
539                    target_fk_name = "%s_fk" % target_part
540                else:
541                    target_fk_name = "%s_inverse_fk" % source_part
542
543                columns = list()
544                constraints = list()
545
546                joins = (self.primaryjoin_clauses, self.secondaryjoin_clauses)
547                for num, desc, fk_name in ((0, e1_desc, source_fk_name), 
548                                           (1, e2_desc, target_fk_name)):
549                    fk_colnames = list()
550                    fk_refcols = list()
551               
552                    for key in desc.primary_keys:
553                        pk_col = key.column
554                       
555                        colname = '%s_%s' % (desc.tablename, pk_col.name)
556
557                        # In case we have a many-to-many self-reference, we
558                        # need to tweak the names of the columns so that we
559                        # don't end up with twice the same column name.
560                        if self.entity is self.target:
561                            colname += str(num + 1)
562
563                        col = Column(colname, pk_col.type)
564                        columns.append(col)
565
566                        # Build the list of local columns which will be part
567                        # of the foreign key.
568                        fk_colnames.append(colname)
569
570                        # Build the list of columns the foreign key will point
571                        # to.
572                        fk_refcols.append(desc.tablename + '.' + pk_col.name)
573
574                        # Build join clauses (in case we have a self-ref)
575                        if self.entity is self.target:
576                            joins[num].append(col == pk_col)
577                   
578                    constraints.append(
579                        ForeignKeyConstraint(fk_colnames, fk_refcols,
580                                             name=fk_name))
581
582                args = columns + constraints
583               
584                self.secondary_table = Table(tablename, e1_desc.metadata, 
585                                             *args)
586
587    def _reflect_table(self, tablename):
588        if not self.target._descriptor.autoload:
589            raise Exception(
590                "Entity '%s' is autoloaded and its '%s' "
591                "has_and_belongs_to_many relationship points to "
592                "the '%s' entity which is not autoloaded"
593                % (self.entity.__name__, self.name,
594                   self.target.__name__))
595               
596        self.secondary_table = Table(tablename, 
597                                     self.entity._descriptor.metadata,
598                                     autoload=True)
599
600        # In the case we have a self-reference, we need to build join clauses
601        if self.entity is self.target:
602            joins = (self.primaryjoin_clauses, 
603                     self.secondaryjoin_clauses)
604            join_nr = 0
605
606            # We loop through all the table constraints. The first
607            # ForeignKeyConstraint we find which points to the entity table
608            # will be used to build the primary join and the second one for
609            # the secondary join.
610            for constraint in self.secondary_table.constraints:
611                if isinstance(constraint, ForeignKeyConstraint):
612                    use_constraint = False
613                    for fk in constraint.elements:
614                        if fk.references(self.entity.table):
615                            use_constraint = True
616                    if use_constraint:
617                        for fk in constraint.elements:
618                            joins[join_nr].append(fk.parent == 
619                                                  fk.column)
620                        join_nr += 1
621
622    def create_properties(self):
623        kwargs = self.kwargs
624
625        if self.target is self.entity:
626            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
627            kwargs['secondaryjoin'] = and_(*self.secondaryjoin_clauses)
628
629        if 'order_by' in kwargs:
630            kwargs['order_by'] = \
631                self.target._descriptor.translate_order_by(kwargs['order_by'])
632
633        self.property = relation(self.target, secondary=self.secondary_table,
634                                 uselist=True, **kwargs)
635        self.entity.mapper.add_property(self.name, self.property)
636
637    def is_inverse(self, other):
638        return super(HasAndBelongsToMany, self).is_inverse(other) and \
639               (self.user_tablename == other.user_tablename or 
640                (not self.user_tablename and not other.user_tablename))
641
642
643belongs_to = Statement(BelongsTo)
644has_one = Statement(HasOne)
645has_many = Statement(HasMany)
646has_and_belongs_to_many = Statement(HasAndBelongsToMany)
Note: See TracBrowser for help on using the browser.