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

Revision 221, 32.9 kB (checked in by ged, 6 years ago)

- added the possibility to not have the autosetup triggers installer (use the

autosetup=False option). Attribute triggers are also done differently so
that attribute access is normal now after the setup phase is over
(get_attribute is not overridden anymore)

- changed "session" and "metadata" module-level attributes to "session"

and "metadata". This WILL break code (especially for users, I think) but
in the long term, I think it's better (less confusing/error-prone) for
everybody

- splitted the statement concept into two distinct concepts: ClassMutator and

EntityBuilder [more information on this on the Wiki soon].

  • this simplifies the simple statements (options, ...) quite a bit.
  • for backward compatibility (if people wrote extensions), the name
    "Statement" stays intact and does approximately the same as before.
  • EntityBuilder's can add columns, properties and so on to the entity, but
    should do so when they are asked to do so (ie, in the correct
    method).
  • There is a generic EntityBuilder named "Property" meant for "statements"
    which can be defined as attributes on the class.
  • All Relationship's and Field's now use this. This has the consequence that
    fields are not used anymore as a way to add columns: columns should be
    added directly. It also has the "side effect" that fields
    in the database are in the same order as they are declared in the
    Entity.

- added shortcut for column_properies: they can now be defined as:

class A(Entity):

firstname = Field(String(20))
lastname = Field(String(20))
myprop = ColumnProperty(lambda c: (c.firstname + ' ' + c.lastname))
instead of
myprop = has_property(lambda c: column_property((c.firstname + ' ' + c.lastname).label('myprop'))

- added *experimental* support to clean a subset of all entities, so that they

can be set up again. This should ONLY be used when you have several totally
unrelated list of entities (no relationships between the entities of the
different lists).

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