root / elixir / tags / 0.6.0 / elixir / relationships.py

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