root / elixir / tags / 0.5.0 / elixir / relationships.py

Revision 269, 36.9 kB (checked in by ged, 4 years ago)

- the columns created by ManyToOne relationships create an index by default,

but this is only a default now instead of an hardcoded value: it can be
disabled now (through column_kwargs). Suggestion by Jason R. Coombs.

- added example of GenericProperty in docstring

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