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

Revision 343, 37.5 kB (checked in by ged, 5 years ago)

Some cleanup to the target/collection stuff... Still need some work to get the
getattr test to pass.

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        self.property = relation(self.target, **kwargs)
416        self.entity._descriptor.add_property(self.name, self.property)
417
418    def target(self):
419        if not self._target:
420            if isinstance(self.of_kind, EntityMeta):
421                self._target = self.of_kind
422            else:
423                collection = self.entity._descriptor.collection
424                self._target = collection.resolve(self.of_kind, self.entity)
425        return self._target
426    target = property(target)
427   
428    def inverse(self):
429        if not self._inverse:
430            if self.inverse_name:
431                desc = self.target._descriptor
432                inverse = desc.find_relationship(self.inverse_name)
433                if inverse is None:
434                    raise Exception(
435                              "Couldn't find a relationship named '%s' in "
436                              "entity '%s' or its parent entities." 
437                              % (self.inverse_name, self.target.__name__))
438                assert self.match_type_of(inverse)
439            else:
440                inverse = self.target._descriptor.get_inverse_relation(self)
441
442            if inverse:
443                self._inverse = inverse
444                inverse._inverse = self
445       
446        return self._inverse
447    inverse = property(inverse)
448   
449    def match_type_of(self, other):
450        return False
451
452    def is_inverse(self, other):
453        return other is not self and \
454               self.match_type_of(other) and \
455               self.entity == other.target and \
456               other.entity == self.target and \
457               (self.inverse_name == other.name or not self.inverse_name) and \
458               (other.inverse_name == self.name or not other.inverse_name)
459
460
461class ManyToOne(Relationship):
462    '''
463   
464    '''
465   
466    def __init__(self, *args, **kwargs):
467        self.colname = kwargs.pop('colname', [])
468        if self.colname and not isinstance(self.colname, list):
469            self.colname = [self.colname]
470
471        self.column_kwargs = kwargs.pop('column_kwargs', {})
472        if 'required' in kwargs:
473            self.column_kwargs['nullable'] = not kwargs.pop('required')
474        if 'primary_key' in kwargs:
475            self.column_kwargs['primary_key'] = kwargs.pop('primary_key')
476        # by default, columns created will have an index.
477        self.column_kwargs.setdefault('index', True)
478
479        self.constraint_kwargs = kwargs.pop('constraint_kwargs', {})
480        if 'use_alter' in kwargs:
481            self.constraint_kwargs['use_alter'] = kwargs.pop('use_alter')
482       
483        if 'ondelete' in kwargs:
484            self.constraint_kwargs['ondelete'] = kwargs.pop('ondelete')
485        if 'onupdate' in kwargs:
486            self.constraint_kwargs['onupdate'] = kwargs.pop('onupdate')
487       
488        self.foreign_key = list()
489        self.primaryjoin_clauses = list()
490
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            pks = target_desc.primary_keys
545            if not pks:
546                raise Exception("No primary key found in target table ('%s') "
547                                "for the '%s' relationship of the '%s' entity."
548                                % (self.target.tablename, self.name, 
549                                   self.entity.__name__))
550
551            for key_num, pk_col in enumerate(pks):
552                if self.colname:
553                    colname = self.colname[key_num]
554                else:
555                    colname = options.FKCOL_NAMEFORMAT % \
556                              {'relname': self.name, 
557                               'key': pk_col.key}
558
559                # We can't add the column to the table directly as the table
560                # might not be created yet.
561                col = Column(colname, pk_col.type, **self.column_kwargs)
562                source_desc.add_column(col)
563
564                # Build the list of local columns which will be part of
565                # the foreign key
566                self.foreign_key.append(col)
567
568                # Store the names of those columns
569                fk_colnames.append(col.key)
570
571                # Build the list of column "paths" the foreign key will
572                # point to
573                target_path = "%s.%s" % (target_desc.tablename, pk_col.key)
574                schema = target_desc.table_options.get('schema', None)
575                if schema is not None:
576                    target_path = "%s.%s" % (schema, target_path)
577                fk_refcols.append(target_path)
578
579                # Build up the primary join. This is needed when you have
580                # several belongs_to relationships between two objects
581                self.primaryjoin_clauses.append(col == pk_col)
582           
583            if 'name' not in self.constraint_kwargs:
584                # In some databases (at least MySQL) the constraint name needs
585                # to be unique for the whole database, instead of per table.
586                fk_name = options.CONSTRAINT_NAMEFORMAT % \
587                          {'tablename': source_desc.tablename, 
588                           'colnames': '_'.join(fk_colnames)}
589                self.constraint_kwargs['name'] = fk_name
590               
591            source_desc.add_constraint(
592                ForeignKeyConstraint(fk_colnames, fk_refcols,
593                                     **self.constraint_kwargs))
594
595    def get_prop_kwargs(self):
596        kwargs = {'uselist': False}
597       
598        if self.entity.table is self.target.table:
599            kwargs['remote_side'] = \
600                [col for col in self.target.table.primary_key.columns]
601
602        if self.primaryjoin_clauses:
603            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
604
605        kwargs.update(self.kwargs)
606
607        return kwargs
608
609
610class OneToOne(Relationship):
611    uselist = False
612
613    def match_type_of(self, other):
614        return isinstance(other, ManyToOne)
615
616    def create_keys(self, pk):
617        # make sure an inverse relationship exists
618        if self.inverse is None:
619            raise Exception(
620                      "Couldn't find any relationship in '%s' which "
621                      "match as inverse of the '%s' relationship "
622                      "defined in the '%s' entity. If you are using "
623                      "inheritance you "
624                      "might need to specify inverse relationships "
625                      "manually by using the inverse keyword."
626                      % (self.target.__name__, self.name,
627                         self.entity.__name__))
628   
629    def get_prop_kwargs(self):
630        kwargs = {'uselist': self.uselist}
631       
632        #TODO: for now, we don't break any test if we remove those 2 lines.
633        # So, we should either complete the selfref test to prove that they
634        # are indeed useful, or remove them. It might be they are indeed
635        # useless because of the primaryjoin, and that the remote_side is
636        # already setup in the other way (belongs_to).
637        if self.entity.table is self.target.table:
638            #FIXME: IF this code is of any use, it will probably break for
639            # autoloaded tables
640            kwargs['remote_side'] = self.inverse.foreign_key
641       
642        if self.inverse.primaryjoin_clauses:
643            kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses)
644
645        kwargs.update(self.kwargs)
646
647        return kwargs
648
649
650class OneToMany(OneToOne):
651    uselist = True
652   
653    def get_prop_kwargs(self):
654        kwargs = super(OneToMany, self).get_prop_kwargs()
655
656        if 'order_by' in kwargs:
657            kwargs['order_by'] = \
658                self.target._descriptor.translate_order_by(
659                    kwargs['order_by'])
660
661        return kwargs
662
663
664class ManyToMany(Relationship):
665    uselist = True
666
667    def __init__(self, *args, **kwargs):
668        self.user_tablename = kwargs.pop('tablename', None)
669        self.local_side = kwargs.pop('local_side', [])
670        if self.local_side and not isinstance(self.local_side, list):
671            self.local_side = [self.local_side]
672        self.remote_side = kwargs.pop('remote_side', [])
673        if self.remote_side and not isinstance(self.remote_side, list):
674            self.remote_side = [self.remote_side]
675        self.ondelete = kwargs.pop('ondelete', None)
676        self.onupdate = kwargs.pop('onupdate', None)
677        self.column_format = kwargs.pop('column_format', options.M2MCOL_NAMEFORMAT)
678
679        self.secondary_table = kwargs.pop('table', None)
680        self.primaryjoin_clauses = list()
681        self.secondaryjoin_clauses = list()
682
683        super(ManyToMany, self).__init__(*args, **kwargs)
684
685    def match_type_of(self, other):
686        return isinstance(other, ManyToMany)
687
688    def create_tables(self):
689        # Warning: if the table was specified manually, the join clauses won't
690        # be computed. We might want to autodetect joins based on fk, as for
691        # autoloaded entities
692        if self.secondary_table:
693            return
694       
695        if self.inverse:
696            if self.inverse.secondary_table:
697                self.secondary_table = self.inverse.secondary_table
698                self.primaryjoin_clauses = self.inverse.secondaryjoin_clauses
699                self.secondaryjoin_clauses = self.inverse.primaryjoin_clauses
700                return
701
702        e1_desc = self.entity._descriptor
703        e2_desc = self.target._descriptor
704       
705        # First, we compute the name of the table. Note that some of the
706        # intermediary variables are reused later for the constraint
707        # names.
708       
709        # We use the name of the relation for the first entity
710        # (instead of the name of its primary key), so that we can
711        # have two many-to-many relations between the same objects
712        # without having a table name collision.
713        source_part = "%s_%s" % (e1_desc.tablename, self.name)
714
715        # And we use only the name of the table of the second entity
716        # when there is no inverse, so that a many-to-many relation
717        # can be defined without an inverse.
718        if self.inverse:
719            target_part = "%s_%s" % (e2_desc.tablename, self.inverse.name)
720        else:
721            target_part = e2_desc.tablename
722       
723        if self.user_tablename:
724            tablename = self.user_tablename
725        else:
726            # We need to keep the table name consistent (independant of
727            # whether this relation or its inverse is setup first).
728            if self.inverse and e1_desc.tablename < e2_desc.tablename:
729                tablename = "%s__%s" % (target_part, source_part)
730            else:
731                tablename = "%s__%s" % (source_part, target_part)
732
733        if e1_desc.autoload:
734            self._reflect_table(tablename)
735        else:
736            # We pre-compute the names of the foreign key constraints
737            # pointing to the source (local) entity's table and to the
738            # target's table
739
740            # In some databases (at least MySQL) the constraint names need
741            # to be unique for the whole database, instead of per table.
742            source_fk_name = "%s_fk" % source_part
743            if self.inverse:
744                target_fk_name = "%s_fk" % target_part
745            else:
746                target_fk_name = "%s_inverse_fk" % source_part
747
748            columns = list()
749            constraints = list()
750
751            joins = (self.primaryjoin_clauses, self.secondaryjoin_clauses)
752            for num, desc, fk_name, m2m in (
753                    (0, e1_desc, source_fk_name, self), 
754                    (1, e2_desc, target_fk_name, self.inverse)):
755                fk_colnames = list()
756                fk_refcols = list()
757           
758                for pk_col in desc.primary_keys:
759                    colname = self.column_format % \
760                              {'tablename': desc.tablename,
761                               'key': pk_col.key,
762                               'entity': desc.entity.__name__.lower()}
763                   
764                    # In case we have a many-to-many self-reference, we
765                    # need to tweak the names of the columns so that we
766                    # don't end up with twice the same column name.
767                    if self.entity is self.target:
768                        colname += str(num + 1)
769                   
770                    col = Column(colname, pk_col.type, primary_key=True)
771                    columns.append(col)
772
773                    # Build the list of local columns which will be part
774                    # of the foreign key.
775                    fk_colnames.append(colname)
776
777                    # Build the list of columns the foreign key will point
778                    # to.
779                    fk_refcols.append(desc.tablename + '.' + pk_col.key)
780
781                    # Build join clauses (in case we have a self-ref)
782                    if self.entity is self.target:
783                        joins[num].append(col == pk_col)
784               
785                onupdate = m2m and m2m.onupdate
786                ondelete = m2m and m2m.ondelete
787               
788                constraints.append(
789                    ForeignKeyConstraint(fk_colnames, fk_refcols,
790                                         name=fk_name, onupdate=onupdate, 
791                                         ondelete=ondelete))
792
793            args = columns + constraints
794           
795            self.secondary_table = Table(tablename, e1_desc.metadata, 
796                                         *args)
797
798    def _reflect_table(self, tablename):
799        if not self.target._descriptor.autoload:
800            raise Exception(
801                "Entity '%s' is autoloaded and its '%s' "
802                "ManyToMany relationship points to "
803                "the '%s' entity which is not autoloaded"
804                % (self.entity.__name__, self.name,
805                   self.target.__name__))
806               
807        self.secondary_table = Table(tablename, 
808                                     self.entity._descriptor.metadata,
809                                     autoload=True)
810
811        # In the case we have a self-reference, we need to build join clauses
812        if self.entity is self.target:
813            #CHECKME: maybe we should try even harder by checking if that
814            # information was defined on the inverse relationship)
815            if not self.local_side and not self.remote_side:
816                raise Exception(
817                    "Self-referential ManyToMany "
818                    "relationships in autoloaded entities need to have at "
819                    "least one of either 'local_side' or 'remote_side' "
820                    "argument specified. The '%s' relationship in the '%s' "
821                    "entity doesn't have either."
822                    % (self.name, self.entity.__name__))
823
824            self.primaryjoin_clauses, self.secondaryjoin_clauses = \
825                _get_join_clauses(self.secondary_table, 
826                                  self.local_side, self.remote_side, 
827                                  self.entity.table)
828
829    def get_prop_kwargs(self):
830        kwargs = {'secondary': self.secondary_table, 
831                  'uselist': self.uselist}
832
833        if self.target is self.entity:
834            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
835            kwargs['secondaryjoin'] = and_(*self.secondaryjoin_clauses)
836
837        kwargs.update(self.kwargs)
838
839        if 'order_by' in kwargs:
840            kwargs['order_by'] = \
841                self.target._descriptor.translate_order_by(kwargs['order_by'])
842
843        return kwargs
844
845    def is_inverse(self, other):
846        return super(ManyToMany, self).is_inverse(other) and \
847               (self.user_tablename == other.user_tablename or 
848                (not self.user_tablename and not other.user_tablename))
849
850
851def _get_join_clauses(local_table, local_cols1, local_cols2, target_table):
852    primary_join, secondary_join = [], []
853    cols1 = local_cols1[:]
854    cols1.sort()
855    cols1 = tuple(cols1)
856
857    if local_cols2 is not None:
858        cols2 = local_cols2[:]
859        cols2.sort()
860        cols2 = tuple(cols2)
861    else:
862        cols2 = None
863
864    # Build a map of fk constraints pointing to the correct table.
865    # The map is indexed on the local col names.
866    constraint_map = {}
867    for constraint in local_table.constraints:
868        if isinstance(constraint, ForeignKeyConstraint):
869            use_constraint = True
870            fk_colnames = []
871
872            # if all columns point to the correct table, we use the constraint
873            for fk in constraint.elements:
874                if fk.references(target_table):
875                    # local column key
876                    fk_colnames.append(fk.parent.key)
877                else:
878                    use_constraint = False
879            if use_constraint:
880                fk_colnames.sort()
881                constraint_map[tuple(fk_colnames)] = constraint
882
883    # Either the fk column names match explicitely with the columns given for
884    # one of the joins (primary or secondary), or we assume the current
885    # columns match because the columns for this join were not given and we
886    # know the other join is either not used (is None) or has an explicit
887    # match.
888       
889#TODO: rewrite this. Even with the comment, I don't even understand it myself.
890    for cols, constraint in constraint_map.iteritems():
891        if cols == cols1 or (cols != cols2 and 
892                             not cols1 and (cols2 in constraint_map or
893                                            cols2 is None)):
894            join = primary_join
895        elif cols == cols2 or (cols2 == () and cols1 in constraint_map):
896            join = secondary_join
897        else:
898            continue
899        for fk in constraint.elements:
900            join.append(fk.parent == fk.column)
901    return primary_join, secondary_join
902
903
904def rel_mutator_handler(target):
905    def handler(entity, name, *args, **kwargs):
906        if 'through' in kwargs and 'via' in kwargs:
907            setattr(entity, name, 
908                    association_proxy(kwargs.pop('through'), 
909                                      kwargs.pop('via'),
910                                      **kwargs))
911            return
912        elif 'through' in kwargs or 'via' in kwargs:
913            raise Exception("'through' and 'via' relationship keyword "
914                            "arguments should be used in combination.")
915        rel = target(kwargs.pop('of_kind'), *args, **kwargs)
916        rel.attach(entity, name)
917    return handler
918
919
920belongs_to = ClassMutator(rel_mutator_handler(ManyToOne))
921has_one = ClassMutator(rel_mutator_handler(OneToOne))
922has_many = ClassMutator(rel_mutator_handler(OneToMany))
923has_and_belongs_to_many = ClassMutator(rel_mutator_handler(ManyToMany))
Note: See TracBrowser for help on using the browser.