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

Revision 307, 37.6 kB (checked in by ged, 5 years ago)

- Made Elixir python 2.3 compatible again (based on a patch from

Jason R. Coombs)

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