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

Revision 355, 38.1 kB (checked in by ged, 6 years ago)

- Added support for viewonly relationships (OneToMany and OneToOne).

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| ``column_format``  | Specify an alternate format string for naming the      |
228|                    | columns in the mapping table.  The default value is    |
229|                    | defined in ``elixir.options.M2MCOL_NAMEFORMAT``.  You  |
230|                    | will be passed ``tablename``, ``key``, and ``entity``  |
231|                    | as arguments to the format string.                     |
232+--------------------+--------------------------------------------------------+
233
234
235================
236DSL-based syntax
237================
238
239The following DSL statements provide an alternative way to define relationships
240between your entities. The first argument to all those statements is the name
241of the relationship, the second is the 'kind' of object you are relating to
242(it is usually given using the ``of_kind`` keyword).
243
244`belongs_to`
245------------
246
247The ``belongs_to`` statement is the DSL syntax equivalent to the ManyToOne_
248relationship. As such, it supports all the same arguments as ManyToOne_
249relationships.
250
251.. sourcecode:: python
252
253    class Pet(Entity):
254        belongs_to('feeder', of_kind='Person')
255        belongs_to('owner', of_kind='Person', colname="owner_id")
256
257
258`has_many`
259----------
260
261The ``has_many`` statement is the DSL syntax equivalent to the OneToMany_
262relationship. As such, it supports all the same arguments as OneToMany_
263relationships.
264
265.. sourcecode:: python
266
267    class Person(Entity):
268        belongs_to('parent', of_kind='Person')
269        has_many('children', of_kind='Person')
270
271There is also an alternate form of the ``has_many`` relationship that takes
272only two keyword arguments: ``through`` and ``via`` in order to encourage a
273richer form of many-to-many relationship that is an alternative to the
274``has_and_belongs_to_many`` statement.  Here is an example:
275
276.. sourcecode:: python
277
278    class Person(Entity):
279        has_field('name', Unicode)
280        has_many('assignments', of_kind='Assignment')
281        has_many('projects', through='assignments', via='project')
282
283    class Assignment(Entity):
284        has_field('start_date', DateTime)
285        belongs_to('person', of_kind='Person')
286        belongs_to('project', of_kind='Project')
287
288    class Project(Entity):
289        has_field('title', Unicode)
290        has_many('assignments', of_kind='Assignment')
291
292In the above example, a `Person` has many `projects` through the `Assignment`
293relationship object, via a `project` attribute.
294
295
296`has_one`
297---------
298
299The ``has_one`` statement is the DSL syntax equivalent to the OneToOne_
300relationship. As such, it supports all the same arguments as OneToOne_
301relationships.
302
303.. sourcecode:: python
304
305    class Car(Entity):
306        has_one('gear_stick', of_kind='GearStick', inverse='car')
307
308    class GearStick(Entity):
309        belongs_to('car', of_kind='Car')
310
311
312`has_and_belongs_to_many`
313-------------------------
314
315The ``has_and_belongs_to_many`` statement is the DSL syntax equivalent to the
316ManyToMany_ relationship. As such, it supports all the same arguments as
317ManyToMany_ relationships.
318
319.. sourcecode:: python
320
321    class Article(Entity):
322        has_and_belongs_to_many('tags', of_kind='Tag')
323
324    class Tag(Entity):
325        has_and_belongs_to_many('articles', of_kind='Article')
326
327'''
328
329import sys
330
331from sqlalchemy         import ForeignKeyConstraint, Column, \
332                               Table, and_
333from sqlalchemy.orm     import relation, backref
334from sqlalchemy.ext.associationproxy import association_proxy
335
336import options
337from elixir.statements  import ClassMutator
338from elixir.fields      import Field
339from elixir.properties  import Property
340from elixir.entity      import EntityDescriptor, EntityMeta
341
342
343__doc_all__ = []
344
345class Relationship(Property):
346    '''
347    Base class for relationships.
348    '''
349
350    def __init__(self, of_kind, *args, **kwargs):
351        super(Relationship, self).__init__()
352
353        self.inverse_name = kwargs.pop('inverse', None)
354
355        self.of_kind = of_kind
356
357        self._target = None
358        self._inverse = None
359
360        self.property = None # sqlalchemy property
361        self.backref = None  # sqlalchemy backref
362
363        #TODO: unused for now
364        self.args = args
365        self.kwargs = kwargs
366
367    def attach(self, entity, name):
368        super(Relationship, self).attach(entity, name)
369        entity._descriptor.relationships.append(self)
370
371    def create_pk_cols(self):
372        self.create_keys(True)
373
374    def create_non_pk_cols(self):
375        self.create_keys(False)
376
377    def create_keys(self, pk):
378        '''
379        Subclasses (ie. concrete relationships) may override this method to
380        create foreign keys.
381        '''
382
383    def create_tables(self):
384        '''
385        Subclasses (ie. concrete relationships) may override this method to
386        create secondary tables.
387        '''
388
389    def create_properties(self):
390        '''
391        Subclasses (ie. concrete relationships) may override this method to
392        add properties to the involved entities.
393        '''
394        if self.property or self.backref:
395            return
396
397        kwargs = {}
398        if self.inverse:
399            # check if the inverse was already processed (and thus has already
400            # defined a backref we can use)
401            if self.inverse.backref:
402                kwargs['backref'] = self.inverse.backref
403            else:
404                kwargs = self.get_prop_kwargs()
405
406                # SQLAlchemy doesn't like when 'secondary' is both defined on
407                # the relation and the backref
408                kwargs.pop('secondary', None)
409
410                # define backref for use by the inverse
411                self.backref = backref(self.name, **kwargs)
412                return
413
414        kwargs.update(self.get_prop_kwargs())
415
416        self.property = relation(self.target, **kwargs)
417        self.add_mapper_property(self.name, self.property)
418
419    def target(self):
420        if not self._target:
421            if isinstance(self.of_kind, EntityMeta):
422                self._target = self.of_kind
423            else:
424                collection = self.entity._descriptor.collection
425                self._target = collection.resolve(self.of_kind, self.entity)
426        return self._target
427    target = property(target)
428
429    def inverse(self):
430        if not self._inverse:
431            if self.inverse_name:
432                desc = self.target._descriptor
433                inverse = desc.find_relationship(self.inverse_name)
434                if inverse is None:
435                    raise Exception(
436                              "Couldn't find a relationship named '%s' in "
437                              "entity '%s' or its parent entities."
438                              % (self.inverse_name, self.target.__name__))
439                assert self.match_type_of(inverse)
440            else:
441                inverse = self.target._descriptor.get_inverse_relation(self)
442
443            if inverse:
444                self._inverse = inverse
445                inverse._inverse = self
446
447        return self._inverse
448    inverse = property(inverse)
449
450    def match_type_of(self, other):
451        return False
452
453    def is_inverse(self, other):
454        # viewonly relationships shouldn't match as inverse of anything (so
455        # that no backref is created -- which doesn't make sense in that case)
456        viewonly = self.kwargs.get('viewonly', False) or \
457                   other.kwargs.get('viewonly', False)
458        return not viewonly and \
459               other is not self and \
460               self.match_type_of(other) and \
461               self.entity == other.target and \
462               other.entity == self.target and \
463               (self.inverse_name == other.name or not self.inverse_name) and \
464               (other.inverse_name == self.name or not other.inverse_name)
465
466
467class ManyToOne(Relationship):
468    '''
469
470    '''
471
472    def __init__(self, *args, **kwargs):
473        self.colname = kwargs.pop('colname', [])
474        if self.colname and not isinstance(self.colname, list):
475            self.colname = [self.colname]
476
477        self.column_kwargs = kwargs.pop('column_kwargs', {})
478        if 'required' in kwargs:
479            self.column_kwargs['nullable'] = not kwargs.pop('required')
480        if 'primary_key' in kwargs:
481            self.column_kwargs['primary_key'] = kwargs.pop('primary_key')
482        # by default, columns created will have an index.
483        self.column_kwargs.setdefault('index', True)
484
485        self.constraint_kwargs = kwargs.pop('constraint_kwargs', {})
486        if 'use_alter' in kwargs:
487            self.constraint_kwargs['use_alter'] = kwargs.pop('use_alter')
488
489        if 'ondelete' in kwargs:
490            self.constraint_kwargs['ondelete'] = kwargs.pop('ondelete')
491        if 'onupdate' in kwargs:
492            self.constraint_kwargs['onupdate'] = kwargs.pop('onupdate')
493
494        self.foreign_key = list()
495        self.primaryjoin_clauses = list()
496
497        super(ManyToOne, self).__init__(*args, **kwargs)
498
499    def match_type_of(self, other):
500        return isinstance(other, (OneToMany, OneToOne))
501
502    def create_keys(self, pk):
503        '''
504        Find all primary keys on the target and create foreign keys on the
505        source accordingly.
506        '''
507
508        if self.foreign_key:
509            return
510
511        if self.column_kwargs.get('primary_key', False) != pk:
512            return
513
514        source_desc = self.entity._descriptor
515        #TODO: make this work if target is a pure SA-mapped class
516        # for that, I need:
517        # - the list of primary key columns of the target table (type and name)
518        # - the name of the target table
519        target_desc = self.target._descriptor
520        #make sure the target has all its pk setup up
521        target_desc.create_pk_cols()
522
523        if source_desc.autoload:
524            #TODO: test if this works when colname is a list
525
526            if self.colname:
527                self.primaryjoin_clauses = \
528                    _get_join_clauses(self.entity.table,
529                                      self.colname, None,
530                                      self.target.table)[0]
531                if not self.primaryjoin_clauses:
532                    raise Exception(
533                        "Couldn't find a foreign key constraint in table "
534                        "'%s' using the following columns: %s."
535                        % (self.entity.table.name, ', '.join(self.colname)))
536        else:
537            fk_refcols = list()
538            fk_colnames = list()
539
540            if self.colname and \
541               len(self.colname) != len(target_desc.primary_keys):
542                raise Exception(
543                        "The number of column names provided in the colname "
544                        "keyword argument of the '%s' relationship of the "
545                        "'%s' entity is not the same as the number of columns "
546                        "of the primary key of '%s'."
547                        % (self.name, self.entity.__name__,
548                           self.target.__name__))
549
550            pks = target_desc.primary_keys
551            if not pks:
552                raise Exception("No primary key found in target table ('%s') "
553                                "for the '%s' relationship of the '%s' entity."
554                                % (self.target.tablename, self.name,
555                                   self.entity.__name__))
556
557            for key_num, pk_col in enumerate(pks):
558                if self.colname:
559                    colname = self.colname[key_num]
560                else:
561                    colname = options.FKCOL_NAMEFORMAT % \
562                              {'relname': self.name,
563                               'key': pk_col.key}
564
565                # We can't add the column to the table directly as the table
566                # might not be created yet.
567                col = Column(colname, pk_col.type, **self.column_kwargs)
568                source_desc.add_column(col)
569
570                # Build the list of local columns which will be part of
571                # the foreign key
572                self.foreign_key.append(col)
573
574                # Store the names of those columns
575                fk_colnames.append(col.key)
576
577                # Build the list of column "paths" the foreign key will
578                # point to
579                fk_refcols.append("%s.%s" % \
580                                  (target_desc.table_fullname, pk_col.key))
581
582                # Build up the primary join. This is needed when you have
583                # several belongs_to relationships between two objects
584                self.primaryjoin_clauses.append(col == pk_col)
585
586            if 'name' not in self.constraint_kwargs:
587                # In some databases (at least MySQL) the constraint name needs
588                # to be unique for the whole database, instead of per table.
589                fk_name = options.CONSTRAINT_NAMEFORMAT % \
590                          {'tablename': source_desc.tablename,
591                           'colnames': '_'.join(fk_colnames)}
592                self.constraint_kwargs['name'] = fk_name
593
594            source_desc.add_constraint(
595                ForeignKeyConstraint(fk_colnames, fk_refcols,
596                                     **self.constraint_kwargs))
597
598    def get_prop_kwargs(self):
599        kwargs = {'uselist': False}
600
601        if self.entity.table is self.target.table:
602            kwargs['remote_side'] = \
603                [col for col in self.target.table.primary_key.columns]
604
605        if self.primaryjoin_clauses:
606            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
607
608        kwargs.update(self.kwargs)
609
610        return kwargs
611
612
613class OneToOne(Relationship):
614    uselist = False
615
616    def match_type_of(self, other):
617        return isinstance(other, ManyToOne)
618
619    def create_keys(self, pk):
620        # When using a viewonly relationship, you are on your own: Elixir
621        # doesn't check that a corresponding ManyToOne relationship exists.
622        if self.kwargs.get('viewonly', False):
623            return
624
625        # make sure an inverse relationship exists
626        if self.inverse is None:
627            raise Exception(
628                      "Couldn't find any relationship in '%s' which "
629                      "match as inverse of the '%s' relationship "
630                      "defined in the '%s' entity. If you are using "
631                      "inheritance you "
632                      "might need to specify inverse relationships "
633                      "manually by using the inverse keyword."
634                      % (self.target.__name__, self.name,
635                         self.entity.__name__))
636
637    def get_prop_kwargs(self):
638        kwargs = {'uselist': self.uselist}
639
640        #TODO: for now, we don't break any test if we remove those 2 lines.
641        # So, we should either complete the selfref test to prove that they
642        # are indeed useful, or remove them. It might be they are indeed
643        # useless because of the primaryjoin, and that the remote_side is
644        # already setup in the other way (belongs_to).
645        if self.entity.table is self.target.table:
646            #FIXME: IF this code is of any use, it will probably break for
647            # autoloaded tables
648            kwargs['remote_side'] = self.inverse.foreign_key
649
650        # viewonly relationships do not have any inverse (and they provide
651        # their primaryjoin argument manually anyway).
652        if not self.kwargs.get('viewonly', False):
653            if self.inverse.primaryjoin_clauses:
654                kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses)
655
656        kwargs.update(self.kwargs)
657
658        return kwargs
659
660
661class OneToMany(OneToOne):
662    uselist = True
663
664    def get_prop_kwargs(self):
665        kwargs = super(OneToMany, self).get_prop_kwargs()
666
667        if 'order_by' in kwargs:
668            kwargs['order_by'] = \
669                self.target._descriptor.translate_order_by(
670                    kwargs['order_by'])
671
672        return kwargs
673
674
675class ManyToMany(Relationship):
676    uselist = True
677
678    def __init__(self, *args, **kwargs):
679        self.user_tablename = kwargs.pop('tablename', None)
680        self.local_side = kwargs.pop('local_side', [])
681        if self.local_side and not isinstance(self.local_side, list):
682            self.local_side = [self.local_side]
683        self.remote_side = kwargs.pop('remote_side', [])
684        if self.remote_side and not isinstance(self.remote_side, list):
685            self.remote_side = [self.remote_side]
686        self.ondelete = kwargs.pop('ondelete', None)
687        self.onupdate = kwargs.pop('onupdate', None)
688        self.column_format = kwargs.pop('column_format', options.M2MCOL_NAMEFORMAT)
689
690        self.secondary_table = kwargs.pop('table', None)
691        self.primaryjoin_clauses = list()
692        self.secondaryjoin_clauses = list()
693
694        super(ManyToMany, self).__init__(*args, **kwargs)
695
696    def match_type_of(self, other):
697        return isinstance(other, ManyToMany)
698
699    def create_tables(self):
700        # Warning: if the table was specified manually, the join clauses won't
701        # be computed. We might want to autodetect joins based on fk, as for
702        # autoloaded entities
703        if self.secondary_table:
704            return
705
706        if self.inverse:
707            if self.inverse.secondary_table:
708                self.secondary_table = self.inverse.secondary_table
709                self.primaryjoin_clauses = self.inverse.secondaryjoin_clauses
710                self.secondaryjoin_clauses = self.inverse.primaryjoin_clauses
711                return
712
713        e1_desc = self.entity._descriptor
714        e2_desc = self.target._descriptor
715
716        e1_schema = e1_desc.table_options.get('schema', None)
717        e2_schema = e2_desc.table_options.get('schema', None)
718        assert e1_schema == e2_schema, \
719               "Schema %r for entity %s differs from schema %r of entity %s" \
720               % (e1_schema, self.entity.__name__,
721                  e2_schema, self.target.__name__)
722
723        # First, we compute the name of the table. Note that some of the
724        # intermediary variables are reused later for the constraint
725        # names.
726
727        # We use the name of the relation for the first entity
728        # (instead of the name of its primary key), so that we can
729        # have two many-to-many relations between the same objects
730        # without having a table name collision.
731        source_part = "%s_%s" % (e1_desc.tablename, self.name)
732
733        # And we use only the name of the table of the second entity
734        # when there is no inverse, so that a many-to-many relation
735        # can be defined without an inverse.
736        if self.inverse:
737            target_part = "%s_%s" % (e2_desc.tablename, self.inverse.name)
738        else:
739            target_part = e2_desc.tablename
740
741        if self.user_tablename:
742            tablename = self.user_tablename
743        else:
744            # We need to keep the table name consistent (independant of
745            # whether this relation or its inverse is setup first).
746            if self.inverse and e1_desc.tablename < e2_desc.tablename:
747                tablename = "%s__%s" % (target_part, source_part)
748            else:
749                tablename = "%s__%s" % (source_part, target_part)
750
751        if e1_desc.autoload:
752            self._reflect_table(tablename)
753        else:
754            # We pre-compute the names of the foreign key constraints
755            # pointing to the source (local) entity's table and to the
756            # target's table
757
758            # In some databases (at least MySQL) the constraint names need
759            # to be unique for the whole database, instead of per table.
760            source_fk_name = "%s_fk" % source_part
761            if self.inverse:
762                target_fk_name = "%s_fk" % target_part
763            else:
764                target_fk_name = "%s_inverse_fk" % source_part
765
766            columns = list()
767            constraints = list()
768
769            joins = (self.primaryjoin_clauses, self.secondaryjoin_clauses)
770            for num, desc, fk_name, rel in (
771                    (0, e1_desc, source_fk_name, self),
772                    (1, e2_desc, target_fk_name, self.inverse)):
773                fk_colnames = list()
774                fk_refcols = list()
775
776                for pk_col in desc.primary_keys:
777                    colname = self.column_format % \
778                              {'tablename': desc.tablename,
779                               'key': pk_col.key,
780                               'entity': desc.entity.__name__.lower()}
781
782                    # In case we have a many-to-many self-reference, we
783                    # need to tweak the names of the columns so that we
784                    # don't end up with twice the same column name.
785                    if self.entity is self.target:
786                        colname += str(num + 1)
787
788                    col = Column(colname, pk_col.type, primary_key=True)
789                    columns.append(col)
790
791                    # Build the list of local columns which will be part
792                    # of the foreign key.
793                    fk_colnames.append(colname)
794
795                    # Build the list of column "paths" the foreign key will
796                    # point to
797                    target_path = "%s.%s" % (desc.table_fullname, pk_col.key)
798                    fk_refcols.append(target_path)
799
800                    # Build join clauses (in case we have a self-ref)
801                    if self.entity is self.target:
802                        joins[num].append(col == pk_col)
803
804                onupdate = rel and rel.onupdate
805                ondelete = rel and rel.ondelete
806
807                constraints.append(
808                    ForeignKeyConstraint(fk_colnames, fk_refcols,
809                                         name=fk_name, onupdate=onupdate,
810                                         ondelete=ondelete))
811
812            args = columns + constraints
813
814            self.secondary_table = Table(tablename, e1_desc.metadata,
815                                         schema=e1_schema, *args)
816
817    def _reflect_table(self, tablename):
818        if not self.target._descriptor.autoload:
819            raise Exception(
820                "Entity '%s' is autoloaded and its '%s' "
821                "ManyToMany relationship points to "
822                "the '%s' entity which is not autoloaded"
823                % (self.entity.__name__, self.name,
824                   self.target.__name__))
825
826        self.secondary_table = Table(tablename,
827                                     self.entity._descriptor.metadata,
828                                     autoload=True)
829
830        # In the case we have a self-reference, we need to build join clauses
831        if self.entity is self.target:
832            #CHECKME: maybe we should try even harder by checking if that
833            # information was defined on the inverse relationship)
834            if not self.local_side and not self.remote_side:
835                raise Exception(
836                    "Self-referential ManyToMany "
837                    "relationships in autoloaded entities need to have at "
838                    "least one of either 'local_side' or 'remote_side' "
839                    "argument specified. The '%s' relationship in the '%s' "
840                    "entity doesn't have either."
841                    % (self.name, self.entity.__name__))
842
843            self.primaryjoin_clauses, self.secondaryjoin_clauses = \
844                _get_join_clauses(self.secondary_table,
845                                  self.local_side, self.remote_side,
846                                  self.entity.table)
847
848    def get_prop_kwargs(self):
849        kwargs = {'secondary': self.secondary_table,
850                  'uselist': self.uselist}
851
852        if self.target is self.entity:
853            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
854            kwargs['secondaryjoin'] = and_(*self.secondaryjoin_clauses)
855
856        kwargs.update(self.kwargs)
857
858        if 'order_by' in kwargs:
859            kwargs['order_by'] = \
860                self.target._descriptor.translate_order_by(kwargs['order_by'])
861
862        return kwargs
863
864    def is_inverse(self, other):
865        return super(ManyToMany, self).is_inverse(other) and \
866               (self.user_tablename == other.user_tablename or
867                (not self.user_tablename and not other.user_tablename))
868
869
870def _get_join_clauses(local_table, local_cols1, local_cols2, target_table):
871    primary_join, secondary_join = [], []
872    cols1 = local_cols1[:]
873    cols1.sort()
874    cols1 = tuple(cols1)
875
876    if local_cols2 is not None:
877        cols2 = local_cols2[:]
878        cols2.sort()
879        cols2 = tuple(cols2)
880    else:
881        cols2 = None
882
883    # Build a map of fk constraints pointing to the correct table.
884    # The map is indexed on the local col names.
885    constraint_map = {}
886    for constraint in local_table.constraints:
887        if isinstance(constraint, ForeignKeyConstraint):
888            use_constraint = True
889            fk_colnames = []
890
891            # if all columns point to the correct table, we use the constraint
892            for fk in constraint.elements:
893                if fk.references(target_table):
894                    # local column key
895                    fk_colnames.append(fk.parent.key)
896                else:
897                    use_constraint = False
898            if use_constraint:
899                fk_colnames.sort()
900                constraint_map[tuple(fk_colnames)] = constraint
901
902    # Either the fk column names match explicitely with the columns given for
903    # one of the joins (primary or secondary), or we assume the current
904    # columns match because the columns for this join were not given and we
905    # know the other join is either not used (is None) or has an explicit
906    # match.
907
908#TODO: rewrite this. Even with the comment, I don't even understand it myself.
909    for cols, constraint in constraint_map.iteritems():
910        if cols == cols1 or (cols != cols2 and
911                             not cols1 and (cols2 in constraint_map or
912                                            cols2 is None)):
913            join = primary_join
914        elif cols == cols2 or (cols2 == () and cols1 in constraint_map):
915            join = secondary_join
916        else:
917            continue
918        for fk in constraint.elements:
919            join.append(fk.parent == fk.column)
920    return primary_join, secondary_join
921
922
923def rel_mutator_handler(target):
924    def handler(entity, name, *args, **kwargs):
925        if 'through' in kwargs and 'via' in kwargs:
926            setattr(entity, name,
927                    association_proxy(kwargs.pop('through'),
928                                      kwargs.pop('via'),
929                                      **kwargs))
930            return
931        elif 'through' in kwargs or 'via' in kwargs:
932            raise Exception("'through' and 'via' relationship keyword "
933                            "arguments should be used in combination.")
934        rel = target(kwargs.pop('of_kind'), *args, **kwargs)
935        rel.attach(entity, name)
936    return handler
937
938
939belongs_to = ClassMutator(rel_mutator_handler(ManyToOne))
940has_one = ClassMutator(rel_mutator_handler(OneToOne))
941has_many = ClassMutator(rel_mutator_handler(OneToMany))
942has_and_belongs_to_many = ClassMutator(rel_mutator_handler(ManyToMany))
Note: See TracBrowser for help on using the browser.