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

Revision 236, 36.5 kB (checked in by ged, 7 years ago)

- migrate to attribute-based syntax in all examples and documentation.
- completed doc

Line 
1'''
2This module provides support for defining relationships between your Elixir
3entities.  Elixir currently supports two syntaxes to do so: the default
4`Attribute-based syntax`_ which supports the following types of relationships:
5ManyToOne_, OneToMany_, OneToOne_ and ManyToMany_, as well as a
6`DSL-based syntax`_ which provides the following statements: belongs_to_,
7has_many_, has_one_ and has_and_belongs_to_many_.
8
9======================
10Attribute-based syntax
11======================
12
13The first argument to all these "normal" relationship classes is the name of
14the class (entity) you are relating to.
15
16Following that first mandatory argument, any number of additional keyword
17arguments can be specified for advanced behavior. See each relationship type
18for a list of their specific keyword arguments. At this point, we'll just note
19that all the arguments that are not specifically processed by Elixir, as
20mentioned in the documentation below are passed on to the SQLAlchemy
21``relation`` function. So, please refer to the `SQLAlchemy relation function's
22documentation <http://www.sqlalchemy.org/docs/04/sqlalchemy_orm.html
23#docstrings_sqlalchemy.orm_modfunc_relation>`_ for further detail about which
24keyword arguments are supported.
25
26You should keep in mind that the following
27keyword arguments are automatically generated by Elixir and should not be used
28unless you want to override the value provided by Elixir: ``uselist``,
29``remote_side``, ``secondary``, ``primaryjoin`` and ``secondaryjoin``.
30
31Additionally, if you want a bidirectionnal relationship, you should define the
32inverse relationship on the other entity explicitly (as opposed to how
33SQLAlchemy's backrefs are defined). In non-ambiguous situations, Elixir will
34match relationships together automatically. If there are several relationships
35of the same type between two entities, Elixir is not able to determine which
36relationship is the inverse of which, so you have to disambiguate the
37situation by giving the name of the inverse relationship in the ``inverse``
38keyword argument.
39
40Here is a detailed explanation of each relation type:
41
42`ManyToOne`
43-----------
44
45Describes the child's side of a parent-child relationship.  For example,
46a `Pet` object may belong to its owner, who is a `Person`.  This could be
47expressed like so:
48
49.. sourcecode:: python
50
51    class Pet(Entity):
52        owner = ManyToOne('Person')
53
54Behind the scene, assuming the primary key of the `Person` entity is
55an integer column named `id`, the ``ManyToOne`` relationship will
56automatically add an integer column named `owner_id` to the entity, with a
57foreign key referencing the `id` column of the `Person` entity.
58
59In addition to the keyword arguments inherited from SQLAlchemy's relation
60function, ``ManyToOne`` relationships accept the following optional arguments
61which will be directed to the created column:
62
63+----------------------+------------------------------------------------------+
64| Option Name          | Description                                          |
65+======================+======================================================+
66| ``colname``          | Specify a custom column name.                        |
67+----------------------+------------------------------------------------------+
68| ``required``         | Specify whether or not this field can be set to None |
69|                      | (left without a value). Defaults to ``False``,       |
70|                      | unless the field is a primary key.                   |
71+----------------------+------------------------------------------------------+
72| ``primary_key``      | Specify whether or not the column(s) created by this |
73|                      | relationship should act as a primary_key.            |
74|                      | Defaults to ``False``.                               |
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| ``ondelete``         | Value for the foreign key constraint ondelete clause.|
93|                      | May be one of: ``cascade``, ``restrict``,            |
94|                      | ``set null``, or ``set default``.                    |
95+----------------------+------------------------------------------------------+
96| ``onupdate``         | Value for the foreign key constraint onupdate clause.|
97|                      | May be one of: ``cascade``, ``restrict``,            |
98|                      | ``set null``, or ``set default``.                    |
99+----------------------+------------------------------------------------------+
100| ``constraint_kwargs``| A dictionary holding any other keyword argument you  |
101|                      | might want to pass to the Constraint.                |
102+----------------------+------------------------------------------------------+
103
104Additionally, Elixir supports the belongs_to_ statement as an alternative,
105DSL-based, syntax to define ManyToOne_ relationships.
106
107
108`OneToMany`
109-----------
110
111Describes the parent's side of a parent-child relationship when there can be
112several children.  For example, a `Person` object has many children, each of
113them being a `Person`. This could be expressed like so:
114
115.. sourcecode:: python
116
117    class Person(Entity):
118        parent = ManyToOne('Person')
119        children = OneToMany('Person')
120
121Note that a ``OneToMany`` relationship **cannot exist** without a
122corresponding ``ManyToOne`` relationship in the other way. This is because the
123``OneToMany`` relationship needs the foreign key created by the ``ManyToOne``
124relationship.
125
126In addition to keyword arguments inherited from SQLAlchemy, ``OneToMany``
127relationships accept the following optional (keyword) arguments:
128
129+--------------------+--------------------------------------------------------+
130| Option Name        | Description                                            |
131+====================+========================================================+
132| ``order_by``       | Specify which field(s) should be used to sort the      |
133|                    | results given by accessing the relation field. You can |
134|                    | either use a string or a list of strings, each         |
135|                    | corresponding to the name of a field in the target     |
136|                    | entity. These field names can optionally be prefixed   |
137|                    | by a minus (for descending order).                     |
138+--------------------+--------------------------------------------------------+
139
140Additionally, Elixir supports an alternate, DSL-based, syntax to define
141OneToMany_ relationships, with the has_many_ statement.
142
143Also, as for standard SQLAlchemy relations, the ``order_by`` keyword argument
144
145
146`OneToOne`
147----------
148
149Describes the parent's side of a parent-child relationship when there is only
150one child.  For example, a `Car` object has one gear stick, which is
151represented as a `GearStick` object. This could be expressed like so:
152
153.. sourcecode:: python
154
155    class Car(Entity):
156        gear_stick = OneToOne('GearStick', inverse='car')
157
158    class GearStick(Entity):
159        car = ManyToOne('Car')
160
161Note that a ``OneToOne`` relationship **cannot exist** without a corresponding
162``ManyToOne`` relationship in the other way. This is because the ``OneToOne``
163relationship needs the foreign_key created by the ``ManyToOne`` relationship.
164
165Additionally, Elixir supports an alternate, DSL-based, syntax to define
166OneToOne_ relationships, with the has_one_ statement.
167
168
169`ManyToMany`
170------------
171
172Describes a relationship in which one kind of entity can be related to several
173objects of the other kind but the objects of that other kind can be related to
174several objects of the first kind.  For example, an `Article` can have several
175tags, but the same `Tag` can be used on several articles.
176
177.. sourcecode:: python
178
179    class Article(Entity):
180        tags = ManyToMany('Tag')
181
182    class Tag(Entity):
183        articles = ManyToMany('Article')
184
185Behind the scene, the ``ManyToMany`` relationship will
186automatically create an intermediate table to host its data.
187
188Note that you don't necessarily need to define the inverse relationship.  In
189our example, even though we want tags to be usable on several articles, we
190might not be interested in which articles correspond to a particular tag.  In
191that case, we could have omitted the `Tag` side of the relationship.
192
193If the entity containing your ``ManyToMany`` relationship is
194autoloaded, you **must** specify at least one of either the ``remote_side`` or
195``local_side`` argument.
196
197In addition to keyword arguments inherited from SQLAlchemy, ``ManyToMany``
198relationships accept the following optional (keyword) arguments:
199
200+--------------------+--------------------------------------------------------+
201| Option Name        | Description                                            |
202+====================+========================================================+
203| ``tablename``      | Specify a custom name for the intermediary table. This |
204|                    | can be used both when the tables needs to be created   |
205|                    | and when the table is autoloaded/reflected from the    |
206|                    | database.                                              |
207+--------------------+--------------------------------------------------------+
208| ``remote_side``    | A column name or list of column names specifying       |
209|                    | which column(s) in the intermediary table are used     |
210|                    | for the "remote" part of a self-referential            |
211|                    | relationship. This argument has an effect only when    |
212|                    | your entities are autoloaded.                          |
213+--------------------+--------------------------------------------------------+
214| ``local_side``     | A column name or list of column names specifying       |
215|                    | which column(s) in the intermediary table are used     |
216|                    | for the "local" part of a self-referential             |
217|                    | relationship. This argument has an effect only when    |
218|                    | your entities are autoloaded.                          |
219+--------------------+--------------------------------------------------------+
220| ``order_by``       | Specify which field(s) should be used to sort the      |
221|                    | results given by accessing the relation field. You can |
222|                    | either use a string or a list of strings, each         |
223|                    | corresponding to the name of a field in the target     |
224|                    | entity. These field names can optionally be prefixed   |
225|                    | by a minus (for descending order).                     |
226+--------------------+--------------------------------------------------------+
227
228================
229DSL-based syntax
230================
231
232The following DSL statements provide an alternative way to define relationships
233between your entities. The first argument to all those statements is the name
234of the relationship, the second is the 'kind' of object you are relating to
235(it is usually given using the ``of_kind`` keyword).
236
237`belongs_to`
238------------
239
240The ``belongs_to`` statement is the DSL syntax equivalent to the ManyToOne_
241relationship. As such, it supports all the same arguments as ManyToOne_
242relationships.
243
244.. sourcecode:: python
245
246    class Pet(Entity):
247        belongs_to('feeder', of_kind='Person')
248        belongs_to('owner', of_kind='Person', colname="owner_id")
249
250
251`has_many`
252----------
253
254The ``has_many`` statement is the DSL syntax equivalent to the OneToMany_
255relationship. As such, it supports all the same arguments as OneToMany_
256relationships.
257
258.. sourcecode:: python
259
260    class Person(Entity):
261        belongs_to('parent', of_kind='Person')
262        has_many('children', of_kind='Person')
263
264There is also an alternate form of the ``has_many`` relationship that takes
265only two keyword arguments: ``through`` and ``via`` in order to encourage a
266richer form of many-to-many relationship that is an alternative to the
267``has_and_belongs_to_many`` statement.  Here is an example:
268
269.. sourcecode:: python
270
271    class Person(Entity):
272        has_field('name', Unicode)
273        has_many('assignments', of_kind='Assignment')
274        has_many('projects', through='assignments', via='project')
275
276    class Assignment(Entity):
277        has_field('start_date', DateTime)
278        belongs_to('person', of_kind='Person')
279        belongs_to('project', of_kind='Project')
280
281    class Project(Entity):
282        has_field('title', Unicode)
283        has_many('assignments', of_kind='Assignment')
284
285In the above example, a `Person` has many `projects` through the `Assignment`
286relationship object, via a `project` attribute.
287
288
289`has_one`
290---------
291
292The ``has_one`` statement is the DSL syntax equivalent to the OneToOne_
293relationship. As such, it supports all the same arguments as OneToOne_
294relationships.
295
296.. sourcecode:: python
297
298    class Car(Entity):
299        has_one('gear_stick', of_kind='GearStick', inverse='car')
300
301    class GearStick(Entity):
302        belongs_to('car', of_kind='Car')
303
304
305`has_and_belongs_to_many`
306-------------------------
307
308The ``has_and_belongs_to_many`` statement is the DSL syntax equivalent to the
309ManyToMany_ relationship. As such, it supports all the same arguments as
310ManyToMany_ relationships.
311
312.. sourcecode:: python
313
314    class Article(Entity):
315        has_and_belongs_to_many('tags', of_kind='Tag')
316
317    class Tag(Entity):
318        has_and_belongs_to_many('articles', of_kind='Article')
319
320'''
321
322from sqlalchemy         import ForeignKeyConstraint, Column, \
323                               Table, and_
324from sqlalchemy.orm     import relation, backref
325from elixir.statements  import ClassMutator
326from elixir.fields      import Field
327from elixir.properties  import Property
328from elixir.entity      import EntityDescriptor, EntityMeta
329from sqlalchemy.ext.associationproxy import association_proxy
330
331import sys
332
333__doc_all__ = []
334
335class Relationship(Property):
336    '''
337    Base class for relationships.
338    '''
339   
340    def __init__(self, of_kind, *args, **kwargs):
341        super(Relationship, self).__init__()
342
343        self.inverse_name = kwargs.pop('inverse', None)
344
345        self.of_kind = of_kind
346
347        self._target = None
348        self._inverse = None
349       
350        self.property = None # sqlalchemy property
351        self.backref = None  # sqlalchemy backref
352
353        #TODO: unused for now
354        self.args = args
355        self.kwargs = kwargs
356
357    def attach(self, entity, name):
358        super(Relationship, self).attach(entity, name)
359        entity._descriptor.relationships.append(self)
360   
361    def create_pk_cols(self):
362        self.create_keys(True)
363
364    def create_non_pk_cols(self):
365        self.create_keys(False)
366
367    def create_keys(self, pk):
368        '''
369        Subclasses (ie. concrete relationships) may override this method to
370        create foreign keys.
371        '''
372   
373    def create_tables(self):
374        '''
375        Subclasses (ie. concrete relationships) may override this method to
376        create secondary tables.
377        '''
378   
379    def create_properties(self):
380        '''
381        Subclasses (ie. concrete relationships) may override this method to
382        add properties to the involved entities.
383        '''
384        if self.property or self.backref:
385            return
386
387        kwargs = {}
388        if self.inverse:
389            # check if the inverse was already processed (and thus has already
390            # defined a backref we can use)
391            if self.inverse.backref:
392                kwargs['backref'] = self.inverse.backref
393            else:
394                kwargs = self.get_prop_kwargs()
395
396                # SQLAlchemy doesn't like when 'secondary' is both defined on
397                # the relation and the backref
398                kwargs.pop('secondary', None)
399
400                # define backref for use by the inverse
401                self.backref = backref(self.name, **kwargs)
402                return
403
404        kwargs.update(self.get_prop_kwargs())
405        self.property = relation(self.target, **kwargs)
406        #TODO: check for duplicate properties
407        self.entity.mapper.add_property(self.name, self.property)
408   
409    def target(self):
410        if not self._target:
411            if isinstance(self.of_kind, EntityMeta):
412                self._target = self.of_kind
413            else:
414                path = self.of_kind.rsplit('.', 1)
415                classname = path.pop()
416
417                if path:
418                    # do we have a fully qualified entity name?
419                    module = sys.modules[path.pop()]
420                    self._target = getattr(module, classname, None)
421                else:
422                    # If not, try the list of entities of the "caller" of the
423                    # source class. Most of the time, this will be the module
424                    # the class is defined in. But it could also be a method
425                    # (inner classes).
426                    caller_entities = EntityMeta._entities[self.entity._caller]
427                    self._target = caller_entities[classname]
428        return self._target
429    target = property(target)
430   
431    def inverse(self):
432        if not self._inverse:
433            if self.inverse_name:
434                desc = self.target._descriptor
435                inverse = desc.find_relationship(self.inverse_name)
436                if inverse is None:
437                    raise Exception(
438                              "Couldn't find a relationship named '%s' in "
439                              "entity '%s' or its parent entities." 
440                              % (self.inverse_name, self.target.__name__))
441                assert self.match_type_of(inverse)
442            else:
443                inverse = self.target._descriptor.get_inverse_relation(self)
444
445            if inverse:
446                self._inverse = inverse
447                inverse._inverse = self
448       
449        return self._inverse
450    inverse = property(inverse)
451   
452    def match_type_of(self, other):
453        return False
454
455    def is_inverse(self, other):
456        return other is not self and \
457               self.match_type_of(other) and \
458               self.entity == other.target and \
459               other.entity == self.target and \
460               (self.inverse_name == other.name or not self.inverse_name) and \
461               (other.inverse_name == self.name or not other.inverse_name)
462
463
464class ManyToOne(Relationship):
465    '''
466   
467    '''
468   
469    def __init__(self, *args, **kwargs):
470        self.colname = kwargs.pop('colname', [])
471        if self.colname and not isinstance(self.colname, list):
472            self.colname = [self.colname]
473
474        self.column_kwargs = kwargs.pop('column_kwargs', {})
475        if 'required' in kwargs:
476            self.column_kwargs['nullable'] = not kwargs.pop('required')
477        if 'primary_key' in kwargs:
478            self.column_kwargs['primary_key'] = kwargs.pop('primary_key')
479
480        self.constraint_kwargs = kwargs.pop('constraint_kwargs', {})
481        if 'use_alter' in kwargs:
482            self.constraint_kwargs['use_alter'] = kwargs.pop('use_alter')
483       
484        if 'ondelete' in kwargs:
485            self.constraint_kwargs['ondelete'] = kwargs.pop('ondelete')
486        if 'onupdate' in kwargs:
487            self.constraint_kwargs['onupdate'] = kwargs.pop('onupdate')
488       
489        self.foreign_key = list()
490        self.primaryjoin_clauses = list()
491        super(ManyToOne, self).__init__(*args, **kwargs)
492   
493    def match_type_of(self, other):
494        return isinstance(other, (OneToMany, OneToOne))
495
496    def create_keys(self, pk):
497        '''
498        Find all primary keys on the target and create foreign keys on the
499        source accordingly.
500        '''
501
502        if self.foreign_key:
503            return
504
505        if self.column_kwargs.get('primary_key', False) != pk:
506            return
507
508        source_desc = self.entity._descriptor
509        #TODO: make this work if target is a pure SA-mapped class
510        # for that, I need:
511        # - the list of primary key columns of the target table (type and name)
512        # - the name of the target table
513        target_desc = self.target._descriptor
514        #make sure the target has all its pk setup up
515        target_desc.create_pk_cols()
516
517        if source_desc.autoload:
518            #TODO: test if this works when colname is a list
519
520            if self.colname:
521                self.primaryjoin_clauses = \
522                    _get_join_clauses(self.entity.table, 
523                                      self.colname, None, 
524                                      self.target.table)[0]
525                if not self.primaryjoin_clauses:
526                    raise Exception(
527                        "Couldn't find a foreign key constraint in table "
528                        "'%s' using the following columns: %s."
529                        % (self.entity.table.name, ', '.join(self.colname)))
530        else:
531            fk_refcols = list()
532            fk_colnames = list()
533
534            if self.colname and \
535               len(self.colname) != len(target_desc.primary_keys):
536                raise Exception(
537                        "The number of column names provided in the colname "
538                        "keyword argument of the '%s' relationship of the "
539                        "'%s' entity is not the same as the number of columns "
540                        "of the primary key of '%s'."
541                        % (self.name, self.entity.__name__, 
542                           self.target.__name__))
543
544            for key_num, pk_col in enumerate(target_desc.primary_keys):
545                if self.colname:
546                    colname = self.colname[key_num]
547                else:
548                    colname = '%s_%s' % (self.name, pk_col.key)
549
550                # we can't add the column to the table directly as the table
551                # might not be created yet.
552                col = Column(colname, pk_col.type, index=True,
553                             **self.column_kwargs)
554                source_desc.add_column(col)
555
556                # build the list of local columns which will be part of
557                # the foreign key
558                self.foreign_key.append(col)
559
560                # store the names of those columns
561                fk_colnames.append(colname)
562
563                # build the list of column "paths" the foreign key will
564                # point to
565                target_path = "%s.%s" % (target_desc.tablename, pk_col.key)
566                schema = target_desc.table_options.get('schema', None)
567                if schema is not None:
568                    target_path = "%s.%s" % (schema, target_path)
569                fk_refcols.append(target_path)
570
571                # build up the primary join. This is needed when you have
572                # several belongs_to relations between two objects
573                self.primaryjoin_clauses.append(col == pk_col)
574           
575            # In some databases (at lease MySQL) the constraint name needs to
576            # be unique for the whole database, instead of per table.
577            fk_name = "%s_%s_fk" % (source_desc.tablename, 
578                                    '_'.join(fk_colnames))
579            source_desc.add_constraint(
580                ForeignKeyConstraint(fk_colnames, fk_refcols, name=fk_name,
581                                     **self.constraint_kwargs))
582
583    def get_prop_kwargs(self):
584        kwargs = {'uselist': False}
585       
586        if self.entity.table is self.target.table:
587            kwargs['remote_side'] = \
588                [col for col in self.target.table.primary_key.columns]
589
590        if self.primaryjoin_clauses:
591            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
592
593        kwargs.update(self.kwargs)
594
595        return kwargs
596
597
598class OneToOne(Relationship):
599    uselist = False
600
601    def match_type_of(self, other):
602        return isinstance(other, ManyToOne)
603
604    def create_keys(self, pk):
605        # make sure an inverse relationship exists
606        if self.inverse is None:
607            raise Exception(
608                      "Couldn't find any relationship in '%s' which "
609                      "match as inverse of the '%s' relationship "
610                      "defined in the '%s' entity. If you are using "
611                      "inheritance you "
612                      "might need to specify inverse relationships "
613                      "manually by using the inverse keyword."
614                      % (self.target.__name__, self.name,
615                         self.entity.__name__))
616   
617    def get_prop_kwargs(self):
618        kwargs = {'uselist': self.uselist}
619       
620        #TODO: for now, we don't break any test if we remove those 2 lines.
621        # So, we should either complete the selfref test to prove that they
622        # are indeed useful, or remove them. It might be they are indeed
623        # useless because of the primaryjoin, and that the remote_side is
624        # already setup in the other way (belongs_to).
625        if self.entity.table is self.target.table:
626            #FIXME: IF this code is of any use, it will probably break for
627            # autoloaded tables
628            kwargs['remote_side'] = self.inverse.foreign_key
629       
630        if self.inverse.primaryjoin_clauses:
631            kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses)
632
633        kwargs.update(self.kwargs)
634
635        return kwargs
636
637
638class OneToMany(OneToOne):
639    uselist = True
640   
641    def get_prop_kwargs(self):
642        kwargs = super(OneToMany, self).get_prop_kwargs()
643
644        if 'order_by' in kwargs:
645            kwargs['order_by'] = \
646                self.target._descriptor.translate_order_by(
647                    kwargs['order_by'])
648
649        return kwargs
650
651
652class ManyToMany(Relationship):
653    uselist = True
654
655    def __init__(self, *args, **kwargs):
656        self.user_tablename = kwargs.pop('tablename', None)
657        self.local_side = kwargs.pop('local_side', [])
658        if self.local_side and not isinstance(self.local_side, list):
659            self.local_side = [self.local_side]
660        self.remote_side = kwargs.pop('remote_side', [])
661        if self.remote_side and not isinstance(self.remote_side, list):
662            self.remote_side = [self.remote_side]
663        self.secondary_table = None
664        self.primaryjoin_clauses = list()
665        self.secondaryjoin_clauses = list()
666        self.ondelete = kwargs.pop('ondelete', None)
667        self.onupdate = kwargs.pop('onupdate', None)
668        super(ManyToMany, self).__init__(*args, **kwargs)
669
670    def match_type_of(self, other):
671        return isinstance(other, ManyToMany)
672
673    def create_tables(self):
674        if self.secondary_table:
675            return
676       
677        if self.inverse:
678            if self.inverse.secondary_table:
679                self.secondary_table = self.inverse.secondary_table
680                self.primaryjoin_clauses = self.inverse.secondaryjoin_clauses
681                self.secondaryjoin_clauses = self.inverse.primaryjoin_clauses
682                return
683
684        e1_desc = self.entity._descriptor
685        e2_desc = self.target._descriptor
686       
687        # First, we compute the name of the table. Note that some of the
688        # intermediary variables are reused later for the constraint
689        # names.
690       
691        # We use the name of the relation for the first entity
692        # (instead of the name of its primary key), so that we can
693        # have two many-to-many relations between the same objects
694        # without having a table name collision.
695        source_part = "%s_%s" % (e1_desc.tablename, self.name)
696
697        # And we use only the name of the table of the second entity
698        # when there is no inverse, so that a many-to-many relation
699        # can be defined without an inverse.
700        if self.inverse:
701            target_part = "%s_%s" % (e2_desc.tablename, self.inverse.name)
702        else:
703            target_part = e2_desc.tablename
704       
705        if self.user_tablename:
706            tablename = self.user_tablename
707        else:
708            # We need to keep the table name consistent (independant of
709            # whether this relation or its inverse is setup first).
710            if self.inverse and e1_desc.tablename < e2_desc.tablename:
711                tablename = "%s__%s" % (target_part, source_part)
712            else:
713                tablename = "%s__%s" % (source_part, target_part)
714
715        if e1_desc.autoload:
716            self._reflect_table(tablename)
717        else:
718            # We pre-compute the names of the foreign key constraints
719            # pointing to the source (local) entity's table and to the
720            # target's table
721
722            # In some databases (at lease MySQL) the constraint names need
723            # to be unique for the whole database, instead of per table.
724            source_fk_name = "%s_fk" % source_part
725            if self.inverse:
726                target_fk_name = "%s_fk" % target_part
727            else:
728                target_fk_name = "%s_inverse_fk" % source_part
729
730            columns = list()
731            constraints = list()
732
733            joins = (self.primaryjoin_clauses, self.secondaryjoin_clauses)
734            for num, desc, fk_name, m2m in ((0, e1_desc, source_fk_name, self), 
735                                            (1, e2_desc, target_fk_name, self.inverse)):
736                fk_colnames = list()
737                fk_refcols = list()
738           
739                for pk_col in desc.primary_keys:
740                    colname = '%s_%s' % (desc.tablename, pk_col.key)
741
742                    # In case we have a many-to-many self-reference, we
743                    # need to tweak the names of the columns so that we
744                    # don't end up with twice the same column name.
745                    if self.entity is self.target:
746                        colname += str(num + 1)
747                   
748                    col = Column(colname, pk_col.type, primary_key=True)
749                    columns.append(col)
750
751                    # Build the list of local columns which will be part
752                    # of the foreign key.
753                    fk_colnames.append(colname)
754
755                    # Build the list of columns the foreign key will point
756                    # to.
757                    fk_refcols.append(desc.tablename + '.' + pk_col.key)
758
759                    # Build join clauses (in case we have a self-ref)
760                    if self.entity is self.target:
761                        joins[num].append(col == pk_col)
762               
763                onupdate = m2m and m2m.onupdate
764                ondelete = m2m and m2m.ondelete
765               
766                constraints.append(
767                    ForeignKeyConstraint(fk_colnames, fk_refcols,
768                                         name=fk_name, onupdate=onupdate, ondelete=ondelete))
769
770            args = columns + constraints
771           
772            self.secondary_table = Table(tablename, e1_desc.metadata, 
773                                         *args)
774
775    def _reflect_table(self, tablename):
776        if not self.target._descriptor.autoload:
777            raise Exception(
778                "Entity '%s' is autoloaded and its '%s' "
779                "has_and_belongs_to_many relationship points to "
780                "the '%s' entity which is not autoloaded"
781                % (self.entity.__name__, self.name,
782                   self.target.__name__))
783               
784        self.secondary_table = Table(tablename, 
785                                     self.entity._descriptor.metadata,
786                                     autoload=True)
787
788        # In the case we have a self-reference, we need to build join clauses
789        if self.entity is self.target:
790            #CHECKME: maybe we should try even harder by checking if that
791            # information was defined on the inverse relationship)
792            if not self.local_side and not self.remote_side:
793                raise Exception(
794                    "Self-referential has_and_belongs_to_many "
795                    "relationships in autoloaded entities need to have at "
796                    "least one of either 'local_side' or 'remote_side' "
797                    "argument specified. The '%s' relationship in the '%s' "
798                    "entity doesn't have either."
799                    % (self.name, self.entity.__name__))
800
801            self.primaryjoin_clauses, self.secondaryjoin_clauses = \
802                _get_join_clauses(self.secondary_table, 
803                                  self.local_side, self.remote_side, 
804                                  self.entity.table)
805
806    def get_prop_kwargs(self):
807        kwargs = {'secondary': self.secondary_table, 
808                  'uselist': self.uselist}
809
810        if self.target is self.entity:
811            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
812            kwargs['secondaryjoin'] = and_(*self.secondaryjoin_clauses)
813
814        kwargs.update(self.kwargs)
815
816        if 'order_by' in kwargs:
817            kwargs['order_by'] = \
818                self.target._descriptor.translate_order_by(kwargs['order_by'])
819
820        return kwargs
821
822    def is_inverse(self, other):
823        return super(ManyToMany, self).is_inverse(other) and \
824               (self.user_tablename == other.user_tablename or 
825                (not self.user_tablename and not other.user_tablename))
826
827
828def _get_join_clauses(local_table, local_cols1, local_cols2, target_table):
829    primary_join, secondary_join = [], []
830    cols1 = local_cols1[:]
831    cols1.sort()
832    cols1 = tuple(cols1)
833
834    if local_cols2 is not None:
835        cols2 = local_cols2[:]
836        cols2.sort()
837        cols2 = tuple(cols2)
838    else:
839        cols2 = None
840
841    # Build a map of fk constraints pointing to the correct table.
842    # The map is indexed on the local col names.
843    constraint_map = {}
844    for constraint in local_table.constraints:
845        if isinstance(constraint, ForeignKeyConstraint):
846
847            use_constraint = True
848            fk_colnames = []
849
850            # if all columns point to the correct table, we use the constraint
851            for fk in constraint.elements:
852                if fk.references(target_table):
853                    fk_colnames.append(fk.parent.key)
854                else:
855                    use_constraint = False
856            if use_constraint:
857                fk_colnames.sort()
858                constraint_map[tuple(fk_colnames)] = constraint
859
860    # Either the fk column names match explicitely with the columns given for
861    # one of the joins (primary or secondary), or we assume the current
862    # columns match because the columns for this join were not given and we
863    # know the other join is either not used (is None) or has an explicit
864    # match.
865       
866#TODO: rewrite this. Even with the comment, I don't even understand it myself.
867    for cols, constraint in constraint_map.iteritems():
868        if cols == cols1 or (cols != cols2 and 
869                             not cols1 and (cols2 in constraint_map or
870                                            cols2 is None)):
871            join = primary_join
872        elif cols == cols2 or (cols2 == () and cols1 in constraint_map):
873            join = secondary_join
874        else:
875            continue
876        for fk in constraint.elements:
877            join.append(fk.parent == fk.column)
878    return primary_join, secondary_join
879
880
881def rel_mutator_handler(target):
882    def handler(entity, name, *args, **kwargs):
883        if 'through' in kwargs and 'via' in kwargs:
884            setattr(entity, name, 
885                    association_proxy(kwargs.pop('through'), 
886                                      kwargs.pop('via'),
887                                      **kwargs))
888            return
889        elif 'through' in kwargs or 'via' in kwargs:
890            raise Exception("'through' and 'via' relationship keyword "
891                            "arguments should be used in combination.")
892        rel = target(kwargs.pop('of_kind'), *args, **kwargs)
893        rel.attach(entity, name)
894    return handler
895
896
897belongs_to = ClassMutator(rel_mutator_handler(ManyToOne))
898has_one = ClassMutator(rel_mutator_handler(OneToOne))
899has_many = ClassMutator(rel_mutator_handler(OneToMany))
900has_and_belongs_to_many = ClassMutator(rel_mutator_handler(ManyToMany))
Note: See TracBrowser for help on using the browser.