| 1 | ''' |
|---|
| 2 | Relationship statements for Elixir entities |
|---|
| 3 | |
|---|
| 4 | ============= |
|---|
| 5 | Relationships |
|---|
| 6 | ============= |
|---|
| 7 | |
|---|
| 8 | This module provides support for defining relationships between your Elixir |
|---|
| 9 | entities. Elixir supports the following types of relationships: belongs_to_, |
|---|
| 10 | has_one_, has_many_ and has_and_belongs_to_many_. |
|---|
| 11 | |
|---|
| 12 | The first argument to all those statements is the name of the relationship, the |
|---|
| 13 | second is the 'kind' of object you are relating to (it is usually given using |
|---|
| 14 | the ``of_kind`` keyword). |
|---|
| 15 | |
|---|
| 16 | Additionally, if you want a bidirectionnal relationship, you should define the |
|---|
| 17 | inverse relationship on the other entity explicitly (as opposed to how |
|---|
| 18 | SQLAlchemy's backrefs are defined). In non-ambiguous situations, Elixir will |
|---|
| 19 | match relationships together automatically. If there are several relationships |
|---|
| 20 | of the same type between two entities, Elixir is not able to determine which |
|---|
| 21 | relationship is the inverse of which, so you have to disambiguate the |
|---|
| 22 | situation by giving the name of the inverse relationship in the ``inverse`` |
|---|
| 23 | keyword argument. |
|---|
| 24 | |
|---|
| 25 | Following these "common" arguments, any number of additional keyword arguments |
|---|
| 26 | can be specified for advanced behavior. The keyword arguments are passed on to |
|---|
| 27 | the SQLAlchemy ``relation`` function. Please refer to the `SQLAlchemy relation |
|---|
| 28 | function's documentation <http://www.sqlalchemy.org/docs/adv_datamapping.myt |
|---|
| 29 | #advdatamapping_properties_relationoptions>`_ for further detail about which |
|---|
| 30 | keyword arguments are supported, but you should keep in mind, the following |
|---|
| 31 | keyword 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 | |
|---|
| 37 | Also, as for standard SQLAlchemy relations, the ``order_by`` keyword argument |
|---|
| 38 | can be used to sort the results given by accessing a relation field (this only |
|---|
| 39 | makes sense for has_many and has_and_belongs_to_many relationships). The value |
|---|
| 40 | of that argument is different though: you can either use a string or a list of |
|---|
| 41 | strings, each corresponding to the name of a field in the target entity. These |
|---|
| 42 | field names can optionally be prefixed by a minus (for descending order). |
|---|
| 43 | |
|---|
| 44 | Here is a detailed explanation of each relation type: |
|---|
| 45 | |
|---|
| 46 | `belongs_to` |
|---|
| 47 | ------------ |
|---|
| 48 | |
|---|
| 49 | Describes the child's side of a parent-child relationship. For example, |
|---|
| 50 | a `Pet` object may belong to its owner, who is a `Person`. This could be |
|---|
| 51 | expressed like so: |
|---|
| 52 | |
|---|
| 53 | :: |
|---|
| 54 | |
|---|
| 55 | class Pet(Entity): |
|---|
| 56 | belongs_to('owner', of_kind='Person') |
|---|
| 57 | |
|---|
| 58 | Behind the scene, assuming the primary key of the `Person` entity is |
|---|
| 59 | an integer column named `id`, the ``belongs_to`` relationship will |
|---|
| 60 | automatically add an integer column named `owner_id` to the entity, with a |
|---|
| 61 | foreign key referencing the `id` column of the `Person` entity. |
|---|
| 62 | |
|---|
| 63 | In addition to the keyword arguments inherited from SQLAlchemy's relation |
|---|
| 64 | function, ``belongs_to`` relationships accept the following optional arguments |
|---|
| 65 | which 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 | |
|---|
| 84 | The following optional arguments are also supported to customize the |
|---|
| 85 | ForeignKeyConstraint 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 | |
|---|
| 111 | Describes the parent's side of a parent-child relationship when there is only |
|---|
| 112 | one child. For example, a `Car` object has one gear stick, which is |
|---|
| 113 | represented 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 | |
|---|
| 123 | Note that an ``has_one`` relationship **cannot exist** without a corresponding |
|---|
| 124 | ``belongs_to`` relationship in the other way. This is because the ``has_one`` |
|---|
| 125 | relationship needs the foreign_key created by the ``belongs_to`` relationship. |
|---|
| 126 | |
|---|
| 127 | `has_many` |
|---|
| 128 | ---------- |
|---|
| 129 | |
|---|
| 130 | Describes the parent's side of a parent-child relationship when there can be |
|---|
| 131 | several children. For example, a `Person` object has many children, each of |
|---|
| 132 | them 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 | |
|---|
| 140 | Note that an ``has_many`` relationship **cannot exist** without a |
|---|
| 141 | corresponding ``belongs_to`` relationship in the other way. This is because the |
|---|
| 142 | ``has_many`` relationship needs the foreign key created by the ``belongs_to`` |
|---|
| 143 | relationship. |
|---|
| 144 | |
|---|
| 145 | There is an alternate form of the ``has_many`` relationship that takes only |
|---|
| 146 | two keyword arguments: `through` and `via` in order to encourage a more |
|---|
| 147 | rich 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 | |
|---|
| 166 | In the above example, a `Person` has many `projects` through the `Assignment` |
|---|
| 167 | relationship object, via a `project` attribute. |
|---|
| 168 | |
|---|
| 169 | `has_and_belongs_to_many` |
|---|
| 170 | ------------------------- |
|---|
| 171 | |
|---|
| 172 | Describes a relationship in which one kind of entity can be related to several |
|---|
| 173 | objects of the other kind but the objects of that other kind can be related to |
|---|
| 174 | several objects of the first kind. For example, an `Article` can have several |
|---|
| 175 | tags, 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 | |
|---|
| 185 | Behind the scene, the ``has_and_belongs_to_many`` relationship will |
|---|
| 186 | automatically create an intermediate table to host its data. |
|---|
| 187 | |
|---|
| 188 | Note that you don't necessarily need to define the inverse relationship. In |
|---|
| 189 | our example, even though we want tags to be usable on several articles, we |
|---|
| 190 | might not be interested in which articles correspond to a particular tag. In |
|---|
| 191 | that case, we could have omitted the `Tag` side of the relationship. |
|---|
| 192 | |
|---|
| 193 | If the entity containg your ``has_and_belongs_to_many`` relationship is |
|---|
| 194 | autoloaded, you **must** specify at least one of either the ``remote_side`` or |
|---|
| 195 | ``local_side`` argument. |
|---|
| 196 | |
|---|
| 197 | In addition to the order_by_ keyword argument, and the other keyword arguments |
|---|
| 198 | inherited from SQLAlchemy, ``has_and_belongs_to_many`` relationships accept |
|---|
| 199 | the 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 | |
|---|
| 224 | from sqlalchemy import ForeignKeyConstraint, Column, \ |
|---|
| 225 | Table, and_ |
|---|
| 226 | from sqlalchemy.orm import relation, backref |
|---|
| 227 | from elixir.statements import ClassMutator |
|---|
| 228 | from elixir.fields import Field |
|---|
| 229 | from elixir.properties import Property |
|---|
| 230 | from elixir.entity import EntityDescriptor, EntityMeta |
|---|
| 231 | from sqlalchemy.ext.associationproxy import association_proxy |
|---|
| 232 | |
|---|
| 233 | import sys |
|---|
| 234 | |
|---|
| 235 | __pudge_all__ = [] |
|---|
| 236 | |
|---|
| 237 | class 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 | |
|---|
| 366 | class 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 | |
|---|
| 500 | class 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 | |
|---|
| 540 | class 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 | |
|---|
| 554 | class 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 | |
|---|
| 730 | def _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 | |
|---|
| 783 | def 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 | |
|---|
| 799 | belongs_to = ClassMutator(rel_mutator_handler(ManyToOne)) |
|---|
| 800 | has_one = ClassMutator(rel_mutator_handler(OneToOne)) |
|---|
| 801 | has_many = ClassMutator(rel_mutator_handler(OneToMany)) |
|---|
| 802 | has_and_belongs_to_many = ClassMutator(rel_mutator_handler(ManyToMany)) |
|---|