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

Revision 297, 37.6 kB (checked in by cleverdevil, 5 years ago)

Added a column_format keyword argument to ManyToMany which can be used to specify an alternate format string for column names in the mapping table. This is required to properly integrate with legacy TurboGears identity databases.

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
338import sys
339import options
340
341__doc_all__ = []
342
343class Relationship(Property):
344    '''
345    Base class for relationships.
346    '''
347   
348    def __init__(self, of_kind, *args, **kwargs):
349        super(Relationship, self).__init__()
350
351        self.inverse_name = kwargs.pop('inverse', None)
352
353        self.of_kind = of_kind
354
355        self._target = None
356        self._inverse = None
357       
358        self.property = None # sqlalchemy property
359        self.backref = None  # sqlalchemy backref
360
361        #TODO: unused for now
362        self.args = args
363        self.kwargs = kwargs
364
365    def attach(self, entity, name):
366        super(Relationship, self).attach(entity, name)
367        entity._descriptor.relationships.append(self)
368   
369    def create_pk_cols(self):
370        self.create_keys(True)
371
372    def create_non_pk_cols(self):
373        self.create_keys(False)
374
375    def create_keys(self, pk):
376        '''
377        Subclasses (ie. concrete relationships) may override this method to
378        create foreign keys.
379        '''
380   
381    def create_tables(self):
382        '''
383        Subclasses (ie. concrete relationships) may override this method to
384        create secondary tables.
385        '''
386   
387    def create_properties(self):
388        '''
389        Subclasses (ie. concrete relationships) may override this method to
390        add properties to the involved entities.
391        '''
392        if self.property or self.backref:
393            return
394
395        kwargs = {}
396        if self.inverse:
397            # check if the inverse was already processed (and thus has already
398            # defined a backref we can use)
399            if self.inverse.backref:
400                kwargs['backref'] = self.inverse.backref
401            else:
402                kwargs = self.get_prop_kwargs()
403
404                # SQLAlchemy doesn't like when 'secondary' is both defined on
405                # the relation and the backref
406                kwargs.pop('secondary', None)
407
408                # define backref for use by the inverse
409                self.backref = backref(self.name, **kwargs)
410                return
411
412        kwargs.update(self.get_prop_kwargs())
413        self.property = relation(self.target, **kwargs)
414        self.entity._descriptor.add_property(self.name, self.property)
415   
416    def target(self):
417        if not self._target:
418            if isinstance(self.of_kind, EntityMeta):
419                self._target = self.of_kind
420            else:
421                path = self.of_kind.rsplit('.', 1)
422                classname = path.pop()
423
424                if path:
425                    # do we have a fully qualified entity name?
426                    module = sys.modules[path.pop()]
427                    self._target = getattr(module, classname, None)
428                else:
429                    # If not, try the list of entities of the "caller" of the
430                    # source class. Most of the time, this will be the module
431                    # the class is defined in. But it could also be a method
432                    # (inner classes).
433                    caller_entities = EntityMeta._entities[self.entity._caller]
434                    self._target = caller_entities[classname]
435        return self._target
436    target = property(target)
437   
438    def inverse(self):
439        if not self._inverse:
440            if self.inverse_name:
441                desc = self.target._descriptor
442                inverse = desc.find_relationship(self.inverse_name)
443                if inverse is None:
444                    raise Exception(
445                              "Couldn't find a relationship named '%s' in "
446                              "entity '%s' or its parent entities." 
447                              % (self.inverse_name, self.target.__name__))
448                assert self.match_type_of(inverse)
449            else:
450                inverse = self.target._descriptor.get_inverse_relation(self)
451
452            if inverse:
453                self._inverse = inverse
454                inverse._inverse = self
455       
456        return self._inverse
457    inverse = property(inverse)
458   
459    def match_type_of(self, other):
460        return False
461
462    def is_inverse(self, other):
463        return other is not self and \
464               self.match_type_of(other) and \
465               self.entity == other.target and \
466               other.entity == self.target and \
467               (self.inverse_name == other.name or not self.inverse_name) and \
468               (other.inverse_name == self.name or not other.inverse_name)
469
470
471class ManyToOne(Relationship):
472    '''
473   
474    '''
475   
476    def __init__(self, *args, **kwargs):
477        self.colname = kwargs.pop('colname', [])
478        if self.colname and not isinstance(self.colname, list):
479            self.colname = [self.colname]
480
481        self.column_kwargs = kwargs.pop('column_kwargs', {})
482        if 'required' in kwargs:
483            self.column_kwargs['nullable'] = not kwargs.pop('required')
484        if 'primary_key' in kwargs:
485            self.column_kwargs['primary_key'] = kwargs.pop('primary_key')
486        # by default, columns created will have an index.
487        self.column_kwargs.setdefault('index', True)
488
489        self.constraint_kwargs = kwargs.pop('constraint_kwargs', {})
490        if 'use_alter' in kwargs:
491            self.constraint_kwargs['use_alter'] = kwargs.pop('use_alter')
492       
493        if 'ondelete' in kwargs:
494            self.constraint_kwargs['ondelete'] = kwargs.pop('ondelete')
495        if 'onupdate' in kwargs:
496            self.constraint_kwargs['onupdate'] = kwargs.pop('onupdate')
497       
498        self.foreign_key = list()
499        self.primaryjoin_clauses = list()
500
501        super(ManyToOne, self).__init__(*args, **kwargs)
502   
503    def match_type_of(self, other):
504        return isinstance(other, (OneToMany, OneToOne))
505
506    def create_keys(self, pk):
507        '''
508        Find all primary keys on the target and create foreign keys on the
509        source accordingly.
510        '''
511
512        if self.foreign_key:
513            return
514
515        if self.column_kwargs.get('primary_key', False) != pk:
516            return
517
518        source_desc = self.entity._descriptor
519        #TODO: make this work if target is a pure SA-mapped class
520        # for that, I need:
521        # - the list of primary key columns of the target table (type and name)
522        # - the name of the target table
523        target_desc = self.target._descriptor
524        #make sure the target has all its pk setup up
525        target_desc.create_pk_cols()
526
527        if source_desc.autoload:
528            #TODO: test if this works when colname is a list
529
530            if self.colname:
531                self.primaryjoin_clauses = \
532                    _get_join_clauses(self.entity.table, 
533                                      self.colname, None, 
534                                      self.target.table)[0]
535                if not self.primaryjoin_clauses:
536                    raise Exception(
537                        "Couldn't find a foreign key constraint in table "
538                        "'%s' using the following columns: %s."
539                        % (self.entity.table.name, ', '.join(self.colname)))
540        else:
541            fk_refcols = list()
542            fk_colnames = list()
543
544            if self.colname and \
545               len(self.colname) != len(target_desc.primary_keys):
546                raise Exception(
547                        "The number of column names provided in the colname "
548                        "keyword argument of the '%s' relationship of the "
549                        "'%s' entity is not the same as the number of columns "
550                        "of the primary key of '%s'."
551                        % (self.name, self.entity.__name__, 
552                           self.target.__name__))
553
554            for key_num, pk_col in enumerate(target_desc.primary_keys):
555                if self.colname:
556                    colname = self.colname[key_num]
557                else:
558                    colname = options.FKCOL_NAMEFORMAT % \
559                              {'relname': self.name, 
560                               'key': pk_col.key}
561
562                # we can't add the column to the table directly as the table
563                # might not be created yet.
564                col = Column(colname, pk_col.type, **self.column_kwargs)
565                source_desc.add_column(col)
566
567                # build the list of local columns which will be part of
568                # the foreign key
569                self.foreign_key.append(col)
570
571                # store the names of those columns
572                fk_colnames.append(colname)
573
574                # build the list of column "paths" the foreign key will
575                # point to
576                target_path = "%s.%s" % (target_desc.tablename, pk_col.key)
577                schema = target_desc.table_options.get('schema', None)
578                if schema is not None:
579                    target_path = "%s.%s" % (schema, target_path)
580                fk_refcols.append(target_path)
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        # make sure an inverse relationship exists
621        if self.inverse is None:
622            raise Exception(
623                      "Couldn't find any relationship in '%s' which "
624                      "match as inverse of the '%s' relationship "
625                      "defined in the '%s' entity. If you are using "
626                      "inheritance you "
627                      "might need to specify inverse relationships "
628                      "manually by using the inverse keyword."
629                      % (self.target.__name__, self.name,
630                         self.entity.__name__))
631   
632    def get_prop_kwargs(self):
633        kwargs = {'uselist': self.uselist}
634       
635        #TODO: for now, we don't break any test if we remove those 2 lines.
636        # So, we should either complete the selfref test to prove that they
637        # are indeed useful, or remove them. It might be they are indeed
638        # useless because of the primaryjoin, and that the remote_side is
639        # already setup in the other way (belongs_to).
640        if self.entity.table is self.target.table:
641            #FIXME: IF this code is of any use, it will probably break for
642            # autoloaded tables
643            kwargs['remote_side'] = self.inverse.foreign_key
644       
645        if self.inverse.primaryjoin_clauses:
646            kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses)
647
648        kwargs.update(self.kwargs)
649
650        return kwargs
651
652
653class OneToMany(OneToOne):
654    uselist = True
655   
656    def get_prop_kwargs(self):
657        kwargs = super(OneToMany, self).get_prop_kwargs()
658
659        if 'order_by' in kwargs:
660            kwargs['order_by'] = \
661                self.target._descriptor.translate_order_by(
662                    kwargs['order_by'])
663
664        return kwargs
665
666
667class ManyToMany(Relationship):
668    uselist = True
669
670    def __init__(self, *args, **kwargs):
671        self.user_tablename = kwargs.pop('tablename', None)
672        self.local_side = kwargs.pop('local_side', [])
673        if self.local_side and not isinstance(self.local_side, list):
674            self.local_side = [self.local_side]
675        self.remote_side = kwargs.pop('remote_side', [])
676        if self.remote_side and not isinstance(self.remote_side, list):
677            self.remote_side = [self.remote_side]
678        self.ondelete = kwargs.pop('ondelete', None)
679        self.onupdate = kwargs.pop('onupdate', None)
680        self.column_format = kwargs.pop('column_format', options.M2MCOL_NAMEFORMAT)
681
682        self.secondary_table = None
683        self.primaryjoin_clauses = list()
684        self.secondaryjoin_clauses = list()
685
686        super(ManyToMany, self).__init__(*args, **kwargs)
687
688    def match_type_of(self, other):
689        return isinstance(other, ManyToMany)
690
691    def create_tables(self):
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                "has_and_belongs_to_many 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 has_and_belongs_to_many "
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
870            use_constraint = True
871            fk_colnames = []
872
873            # if all columns point to the correct table, we use the constraint
874            for fk in constraint.elements:
875                if fk.references(target_table):
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.