| 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 | | ``column_kwargs`` | A dictionary holding any other keyword argument you | |
|---|
| 77 | | | might want to pass to the Column. | |
|---|
| 78 | +----------------------+------------------------------------------------------+ |
|---|
| 79 | |
|---|
| 80 | The following optional arguments are also supported to customize the |
|---|
| 81 | ForeignKeyConstraint 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 | | ``constraint_kwargs``| A dictionary holding any other keyword argument you | |
|---|
| 97 | | | might want to pass to the Constraint. | |
|---|
| 98 | +----------------------+------------------------------------------------------+ |
|---|
| 99 | |
|---|
| 100 | `has_one` |
|---|
| 101 | --------- |
|---|
| 102 | |
|---|
| 103 | Describes the parent's side of a parent-child relationship when there is only |
|---|
| 104 | one child. For example, a `Car` object has one gear stick, which is |
|---|
| 105 | represented as a `GearStick` object. This could be expressed like so: |
|---|
| 106 | |
|---|
| 107 | :: |
|---|
| 108 | |
|---|
| 109 | class Car(Entity): |
|---|
| 110 | has_one('gear_stick', of_kind='GearStick', inverse='car') |
|---|
| 111 | |
|---|
| 112 | class GearStick(Entity): |
|---|
| 113 | belongs_to('car', of_kind='Car') |
|---|
| 114 | |
|---|
| 115 | Note that an ``has_one`` relationship **cannot exist** without a corresponding |
|---|
| 116 | ``belongs_to`` relationship in the other way. This is because the ``has_one`` |
|---|
| 117 | relationship needs the foreign_key created by the ``belongs_to`` relationship. |
|---|
| 118 | |
|---|
| 119 | `has_many` |
|---|
| 120 | ---------- |
|---|
| 121 | |
|---|
| 122 | Describes the parent's side of a parent-child relationship when there can be |
|---|
| 123 | several children. For example, a `Person` object has many children, each of |
|---|
| 124 | them being a `Person`. This could be expressed like so: |
|---|
| 125 | |
|---|
| 126 | :: |
|---|
| 127 | |
|---|
| 128 | class Person(Entity): |
|---|
| 129 | belongs_to('parent', of_kind='Person') |
|---|
| 130 | has_many('children', of_kind='Person') |
|---|
| 131 | |
|---|
| 132 | Note that an ``has_many`` relationship **cannot exist** without a |
|---|
| 133 | corresponding ``belongs_to`` relationship in the other way. This is because the |
|---|
| 134 | ``has_many`` relationship needs the foreign key created by the ``belongs_to`` |
|---|
| 135 | relationship. |
|---|
| 136 | |
|---|
| 137 | `has_and_belongs_to_many` |
|---|
| 138 | ------------------------- |
|---|
| 139 | |
|---|
| 140 | Describes a relationship in which one kind of entity can be related to several |
|---|
| 141 | objects of the other kind but the objects of that other kind can be related to |
|---|
| 142 | several objects of the first kind. For example, an `Article` can have several |
|---|
| 143 | tags, but the same `Tag` can be used on several articles. |
|---|
| 144 | |
|---|
| 145 | :: |
|---|
| 146 | |
|---|
| 147 | class Article(Entity): |
|---|
| 148 | has_and_belongs_to_many('tags', of_kind='Tag') |
|---|
| 149 | |
|---|
| 150 | class Tag(Entity): |
|---|
| 151 | has_and_belongs_to_many('articles', of_kind='Article') |
|---|
| 152 | |
|---|
| 153 | Behind the scene, the ``has_and_belongs_to_many`` relationship will |
|---|
| 154 | automatically create an intermediate table to host its data. |
|---|
| 155 | |
|---|
| 156 | Note that you don't necessarily need to define the inverse relationship. In |
|---|
| 157 | our example, even though we want tags to be usable on several articles, we |
|---|
| 158 | might not be interested in which articles correspond to a particular tag. In |
|---|
| 159 | that case, we could have omitted the `Tag` side of the relationship. |
|---|
| 160 | |
|---|
| 161 | If the entity containg your ``has_and_belongs_to_many`` relationship is |
|---|
| 162 | autoloaded, you **must** specify at least one of either the ``remote_side`` or |
|---|
| 163 | ``local_side`` argument. |
|---|
| 164 | |
|---|
| 165 | In addition to the order_by_ keyword argument, and the other keyword arguments |
|---|
| 166 | inherited from SQLAlchemy, ``has_and_belongs_to_many`` relationships accept |
|---|
| 167 | the following optional (keyword) arguments: |
|---|
| 168 | |
|---|
| 169 | +--------------------+--------------------------------------------------------+ |
|---|
| 170 | | Option Name | Description | |
|---|
| 171 | +====================+========================================================+ |
|---|
| 172 | | ``tablename`` | Specify a custom name for the intermediary table. This | |
|---|
| 173 | | | can be used both when the tables needs to be created | |
|---|
| 174 | | | and when the table is autoloaded/reflected from the | |
|---|
| 175 | | | database. | |
|---|
| 176 | +--------------------+--------------------------------------------------------+ |
|---|
| 177 | | ``remote_side`` | A column name or list of column names specifying | |
|---|
| 178 | | | which column(s) in the intermediary table are used | |
|---|
| 179 | | | for the "remote" part of a self-referential | |
|---|
| 180 | | | relationship. This argument has an effect only when | |
|---|
| 181 | | | your entities are autoloaded. | |
|---|
| 182 | +--------------------+--------------------------------------------------------+ |
|---|
| 183 | | ``local_side`` | A column name or list of column names specifying | |
|---|
| 184 | | | which column(s) in the intermediary table are used | |
|---|
| 185 | | | for the "local" part of a self-referential | |
|---|
| 186 | | | relationship. This argument has an effect only when | |
|---|
| 187 | | | your entities are autoloaded. | |
|---|
| 188 | +--------------------+--------------------------------------------------------+ |
|---|
| 189 | |
|---|
| 190 | ''' |
|---|
| 191 | |
|---|
| 192 | from sqlalchemy import relation, ForeignKeyConstraint, Column, \ |
|---|
| 193 | Table, and_ |
|---|
| 194 | from elixir.statements import Statement |
|---|
| 195 | from elixir.fields import Field |
|---|
| 196 | from elixir.entity import EntityDescriptor |
|---|
| 197 | |
|---|
| 198 | import sys |
|---|
| 199 | |
|---|
| 200 | __pudge_all__ = [] |
|---|
| 201 | |
|---|
| 202 | class Relationship(object): |
|---|
| 203 | ''' |
|---|
| 204 | Base class for relationships. |
|---|
| 205 | ''' |
|---|
| 206 | |
|---|
| 207 | def __init__(self, entity, name, *args, **kwargs): |
|---|
| 208 | self.entity = entity |
|---|
| 209 | self.name = name |
|---|
| 210 | self.of_kind = kwargs.pop('of_kind') |
|---|
| 211 | self.inverse_name = kwargs.pop('inverse', None) |
|---|
| 212 | |
|---|
| 213 | self._target = None |
|---|
| 214 | self._inverse = None |
|---|
| 215 | |
|---|
| 216 | self.property = None # sqlalchemy property |
|---|
| 217 | |
|---|
| 218 | #TODO: unused for now |
|---|
| 219 | self.args = args |
|---|
| 220 | self.kwargs = kwargs |
|---|
| 221 | |
|---|
| 222 | self.entity._descriptor.relationships[self.name] = self |
|---|
| 223 | |
|---|
| 224 | def create_keys(self): |
|---|
| 225 | ''' |
|---|
| 226 | Subclasses (ie. concrete relationships) may override this method to |
|---|
| 227 | create foreign keys. |
|---|
| 228 | ''' |
|---|
| 229 | |
|---|
| 230 | def create_tables(self): |
|---|
| 231 | ''' |
|---|
| 232 | Subclasses (ie. concrete relationships) may override this method to |
|---|
| 233 | create secondary tables. |
|---|
| 234 | ''' |
|---|
| 235 | |
|---|
| 236 | def create_properties(self): |
|---|
| 237 | ''' |
|---|
| 238 | Subclasses (ie. concrete relationships) may override this method to add |
|---|
| 239 | properties to the involved entities. |
|---|
| 240 | ''' |
|---|
| 241 | |
|---|
| 242 | def setup(self): |
|---|
| 243 | ''' |
|---|
| 244 | Sets up the relationship, creates foreign keys and secondary tables. |
|---|
| 245 | ''' |
|---|
| 246 | |
|---|
| 247 | if not self.target: |
|---|
| 248 | return False |
|---|
| 249 | |
|---|
| 250 | if self.property: |
|---|
| 251 | return True |
|---|
| 252 | |
|---|
| 253 | self.create_keys() |
|---|
| 254 | self.create_tables() |
|---|
| 255 | self.create_properties() |
|---|
| 256 | |
|---|
| 257 | return True |
|---|
| 258 | |
|---|
| 259 | def target(self): |
|---|
| 260 | if not self._target: |
|---|
| 261 | path = self.of_kind.rsplit('.', 1) |
|---|
| 262 | classname = path.pop() |
|---|
| 263 | |
|---|
| 264 | if path: |
|---|
| 265 | # do we have a fully qualified entity name? |
|---|
| 266 | module = sys.modules[path.pop()] |
|---|
| 267 | else: |
|---|
| 268 | # if not, try the same module as the source |
|---|
| 269 | module = self.entity._descriptor.module |
|---|
| 270 | |
|---|
| 271 | self._target = getattr(module, classname, None) |
|---|
| 272 | if not self._target: |
|---|
| 273 | # This is ugly but we need it because the class which is |
|---|
| 274 | # currently being defined (we have to keep in mind we are in |
|---|
| 275 | # its metaclass code) is not yet available in the module |
|---|
| 276 | # namespace, so the getattr above fails. And unfortunately, |
|---|
| 277 | # this doesn't only happen for the owning entity of this |
|---|
| 278 | # relation since we might be setting up a deferred relation. |
|---|
| 279 | e = EntityDescriptor.current.entity |
|---|
| 280 | if classname == e.__name__ or \ |
|---|
| 281 | self.of_kind == e.__module__ +'.'+ e.__name__: |
|---|
| 282 | self._target = e |
|---|
| 283 | else: |
|---|
| 284 | return None |
|---|
| 285 | |
|---|
| 286 | return self._target |
|---|
| 287 | target = property(target) |
|---|
| 288 | |
|---|
| 289 | def inverse(self): |
|---|
| 290 | if not self._inverse: |
|---|
| 291 | if self.inverse_name: |
|---|
| 292 | desc = self.target._descriptor |
|---|
| 293 | # we use all_relationships so that relationships from parent |
|---|
| 294 | # entities are included too |
|---|
| 295 | inverse = desc.all_relationships.get(self.inverse_name, None) |
|---|
| 296 | if inverse is None: |
|---|
| 297 | raise Exception( |
|---|
| 298 | "Couldn't find a relationship named '%s' in " |
|---|
| 299 | "entity '%s' or its parent entities." |
|---|
| 300 | % (self.inverse_name, self.target.__name__)) |
|---|
| 301 | assert self.match_type_of(inverse) |
|---|
| 302 | else: |
|---|
| 303 | inverse = self.target._descriptor.get_inverse_relation(self) |
|---|
| 304 | |
|---|
| 305 | if inverse: |
|---|
| 306 | self._inverse = inverse |
|---|
| 307 | inverse._inverse = self |
|---|
| 308 | |
|---|
| 309 | return self._inverse |
|---|
| 310 | inverse = property(inverse) |
|---|
| 311 | |
|---|
| 312 | def match_type_of(self, other): |
|---|
| 313 | t1, t2 = type(self), type(other) |
|---|
| 314 | |
|---|
| 315 | if t1 is HasAndBelongsToMany: |
|---|
| 316 | return t1 is t2 |
|---|
| 317 | elif t1 in (HasOne, HasMany): |
|---|
| 318 | return t2 is BelongsTo |
|---|
| 319 | elif t1 is BelongsTo: |
|---|
| 320 | return t2 in (HasMany, HasOne) |
|---|
| 321 | else: |
|---|
| 322 | return False |
|---|
| 323 | |
|---|
| 324 | def is_inverse(self, other): |
|---|
| 325 | return other is not self and \ |
|---|
| 326 | self.match_type_of(other) and \ |
|---|
| 327 | self.entity == other.target and \ |
|---|
| 328 | other.entity == self.target and \ |
|---|
| 329 | (self.inverse_name == other.name or not self.inverse_name) and \ |
|---|
| 330 | (other.inverse_name == self.name or not other.inverse_name) |
|---|
| 331 | |
|---|
| 332 | |
|---|
| 333 | class BelongsTo(Relationship): |
|---|
| 334 | ''' |
|---|
| 335 | |
|---|
| 336 | ''' |
|---|
| 337 | |
|---|
| 338 | def __init__(self, entity, name, *args, **kwargs): |
|---|
| 339 | self.colname = kwargs.pop('colname', []) |
|---|
| 340 | if self.colname and not isinstance(self.colname, list): |
|---|
| 341 | self.colname = [self.colname] |
|---|
| 342 | |
|---|
| 343 | self.column_kwargs = kwargs.pop('column_kwargs', {}) |
|---|
| 344 | if 'required' in kwargs: |
|---|
| 345 | self.column_kwargs['nullable'] = not kwargs.pop('required') |
|---|
| 346 | |
|---|
| 347 | self.constraint_kwargs = kwargs.pop('constraint_kwargs', {}) |
|---|
| 348 | if 'use_alter' in kwargs: |
|---|
| 349 | self.constraint_kwargs['use_alter'] = kwargs.pop('use_alter') |
|---|
| 350 | |
|---|
| 351 | if 'ondelete' in kwargs: |
|---|
| 352 | self.constraint_kwargs['ondelete'] = kwargs.pop('ondelete') |
|---|
| 353 | |
|---|
| 354 | self.foreign_key = list() |
|---|
| 355 | self.primaryjoin_clauses = list() |
|---|
| 356 | super(BelongsTo, self).__init__(entity, name, *args, **kwargs) |
|---|
| 357 | |
|---|
| 358 | def create_keys(self): |
|---|
| 359 | ''' |
|---|
| 360 | Find all primary keys on the target and create foreign keys on the |
|---|
| 361 | source accordingly. |
|---|
| 362 | ''' |
|---|
| 363 | |
|---|
| 364 | source_desc = self.entity._descriptor |
|---|
| 365 | target_desc = self.target._descriptor |
|---|
| 366 | |
|---|
| 367 | if source_desc.autoload: |
|---|
| 368 | #TODO: test if this works when colname is a list |
|---|
| 369 | if self.colname: |
|---|
| 370 | self.primaryjoin_clauses = \ |
|---|
| 371 | _build_join_clauses(self.entity.table, |
|---|
| 372 | self.colname, None, |
|---|
| 373 | self.target.table)[0] |
|---|
| 374 | if not self.primaryjoin_clauses: |
|---|
| 375 | raise Exception( |
|---|
| 376 | "Couldn't find a foreign key constraint in table " |
|---|
| 377 | "'%s' using the following columns: %s." |
|---|
| 378 | % (self.entity.table.name, ', '.join(self.colname))) |
|---|
| 379 | else: |
|---|
| 380 | fk_refcols = list() |
|---|
| 381 | fk_colnames = list() |
|---|
| 382 | |
|---|
| 383 | if self.colname and \ |
|---|
| 384 | len(self.colname) != len(target_desc.primary_keys): |
|---|
| 385 | raise Exception( |
|---|
| 386 | "The number of column names provided in the colname " |
|---|
| 387 | "keyword argument of the '%s' relationship of the " |
|---|
| 388 | "'%s' entity is not the same as the number of columns " |
|---|
| 389 | "of the primary key of '%s'." |
|---|
| 390 | % (self.name, self.entity.__name__, |
|---|
| 391 | self.target.__name__)) |
|---|
| 392 | |
|---|
| 393 | for key_num, key in enumerate(target_desc.primary_keys): |
|---|
| 394 | pk_col = key.column |
|---|
| 395 | |
|---|
| 396 | if self.colname: |
|---|
| 397 | colname = self.colname[key_num] |
|---|
| 398 | else: |
|---|
| 399 | colname = '%s_%s' % (self.name, pk_col.name) |
|---|
| 400 | |
|---|
| 401 | # we use a Field here instead of using a Column directly |
|---|
| 402 | # because of add_field |
|---|
| 403 | field = Field(pk_col.type, colname=colname, index=True, |
|---|
| 404 | **self.column_kwargs) |
|---|
| 405 | source_desc.add_field(field) |
|---|
| 406 | |
|---|
| 407 | # build the list of local columns which will be part of |
|---|
| 408 | # the foreign key |
|---|
| 409 | self.foreign_key.append(field.column) |
|---|
| 410 | |
|---|
| 411 | # store the names of those columns |
|---|
| 412 | fk_colnames.append(colname) |
|---|
| 413 | |
|---|
| 414 | # build the list of columns the foreign key will point to |
|---|
| 415 | if target_desc.entity.table.schema: |
|---|
| 416 | fk_refcols.append("%s.%s.%s" % ( |
|---|
| 417 | target_desc.entity.table.schema, |
|---|
| 418 | target_desc.entity.table.name, |
|---|
| 419 | pk_col.name)) |
|---|
| 420 | else: |
|---|
| 421 | fk_refcols.append("%s.%s" % (target_desc.entity.table.name, |
|---|
| 422 | pk_col.name)) |
|---|
| 423 | |
|---|
| 424 | # build up the primary join. This is needed when you have |
|---|
| 425 | # several belongs_to relations between two objects |
|---|
| 426 | self.primaryjoin_clauses.append(field.column == pk_col) |
|---|
| 427 | |
|---|
| 428 | # In some databases (at lease MySQL) the constraint name needs to |
|---|
| 429 | # be unique for the whole database, instead of per table. |
|---|
| 430 | fk_name = "%s_%s_fk" % (self.entity.table.name, |
|---|
| 431 | '_'.join(fk_colnames)) |
|---|
| 432 | source_desc.add_constraint(ForeignKeyConstraint( |
|---|
| 433 | fk_colnames, fk_refcols, |
|---|
| 434 | name=fk_name, |
|---|
| 435 | **self.constraint_kwargs)) |
|---|
| 436 | |
|---|
| 437 | def create_properties(self): |
|---|
| 438 | kwargs = self.kwargs |
|---|
| 439 | |
|---|
| 440 | if self.entity.table is self.target.table: |
|---|
| 441 | if self.entity._descriptor.autoload: |
|---|
| 442 | cols = [col for col in self.target.table.primary_key.columns] |
|---|
| 443 | else: |
|---|
| 444 | cols = [k.column for k in self.target._descriptor.primary_keys] |
|---|
| 445 | kwargs['remote_side'] = cols |
|---|
| 446 | |
|---|
| 447 | if self.primaryjoin_clauses: |
|---|
| 448 | kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses) |
|---|
| 449 | kwargs['uselist'] = False |
|---|
| 450 | |
|---|
| 451 | self.property = relation(self.target, **kwargs) |
|---|
| 452 | self.entity.mapper.add_property(self.name, self.property) |
|---|
| 453 | |
|---|
| 454 | |
|---|
| 455 | class HasOne(Relationship): |
|---|
| 456 | uselist = False |
|---|
| 457 | |
|---|
| 458 | def create_keys(self): |
|---|
| 459 | # make sure an inverse relationship exists |
|---|
| 460 | if self.inverse is None: |
|---|
| 461 | raise Exception( |
|---|
| 462 | "Couldn't find any relationship in '%s' which " |
|---|
| 463 | "match as inverse of the '%s' relationship " |
|---|
| 464 | "defined in the '%s' entity. If you are using " |
|---|
| 465 | "inheritance you " |
|---|
| 466 | "might need to specify inverse relationships " |
|---|
| 467 | "manually by using the inverse keyword." |
|---|
| 468 | % (self.target.__name__, self.name, |
|---|
| 469 | self.entity.__name__)) |
|---|
| 470 | # make sure it is set up because it creates the foreign key we'll need |
|---|
| 471 | self.inverse.setup() |
|---|
| 472 | |
|---|
| 473 | def create_properties(self): |
|---|
| 474 | kwargs = self.kwargs |
|---|
| 475 | |
|---|
| 476 | #TODO: for now, we don't break any test if we remove those 2 lines. |
|---|
| 477 | # So, we should either complete the selfref test to prove that they |
|---|
| 478 | # are indeed useful, or remove them. It might be they are indeed |
|---|
| 479 | # useless because of the primaryjoin, and that the remote_side is |
|---|
| 480 | # already setup in the other way (belongs_to). |
|---|
| 481 | if self.entity.table is self.target.table: |
|---|
| 482 | #FIXME: IF this code is of any use, it will probably break for |
|---|
| 483 | # autoloaded tables |
|---|
| 484 | kwargs['remote_side'] = self.inverse.foreign_key |
|---|
| 485 | |
|---|
| 486 | if self.inverse.primaryjoin_clauses: |
|---|
| 487 | kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses) |
|---|
| 488 | |
|---|
| 489 | kwargs['uselist'] = self.uselist |
|---|
| 490 | |
|---|
| 491 | self.property = relation(self.target, **kwargs) |
|---|
| 492 | self.entity.mapper.add_property(self.name, self.property) |
|---|
| 493 | |
|---|
| 494 | |
|---|
| 495 | class HasMany(HasOne): |
|---|
| 496 | uselist = True |
|---|
| 497 | |
|---|
| 498 | def create_properties(self): |
|---|
| 499 | if 'order_by' in self.kwargs: |
|---|
| 500 | self.kwargs['order_by'] = \ |
|---|
| 501 | self.target._descriptor.translate_order_by( |
|---|
| 502 | self.kwargs['order_by']) |
|---|
| 503 | |
|---|
| 504 | super(HasMany, self).create_properties() |
|---|
| 505 | |
|---|
| 506 | |
|---|
| 507 | class HasAndBelongsToMany(Relationship): |
|---|
| 508 | |
|---|
| 509 | def __init__(self, entity, name, *args, **kwargs): |
|---|
| 510 | self.user_tablename = kwargs.pop('tablename', None) |
|---|
| 511 | self.local_side = kwargs.pop('local_side', []) |
|---|
| 512 | if self.local_side and not isinstance(self.local_side, list): |
|---|
| 513 | self.local_side = [self.local_side] |
|---|
| 514 | self.remote_side = kwargs.pop('remote_side', []) |
|---|
| 515 | if self.remote_side and not isinstance(self.remote_side, list): |
|---|
| 516 | self.remote_side = [self.remote_side] |
|---|
| 517 | self.secondary_table = None |
|---|
| 518 | self.primaryjoin_clauses = list() |
|---|
| 519 | self.secondaryjoin_clauses = list() |
|---|
| 520 | super(HasAndBelongsToMany, self).__init__(entity, name, |
|---|
| 521 | *args, **kwargs) |
|---|
| 522 | |
|---|
| 523 | def create_tables(self): |
|---|
| 524 | if self.inverse: |
|---|
| 525 | if self.inverse.secondary_table: |
|---|
| 526 | self.secondary_table = self.inverse.secondary_table |
|---|
| 527 | self.primaryjoin_clauses = self.inverse.secondaryjoin_clauses |
|---|
| 528 | self.secondaryjoin_clauses = self.inverse.primaryjoin_clauses |
|---|
| 529 | |
|---|
| 530 | if not self.secondary_table: |
|---|
| 531 | e1_desc = self.entity._descriptor |
|---|
| 532 | e2_desc = self.target._descriptor |
|---|
| 533 | |
|---|
| 534 | # First, we compute the name of the table. Note that some of the |
|---|
| 535 | # intermediary variables are reused later for the constraint |
|---|
| 536 | # names. |
|---|
| 537 | |
|---|
| 538 | # We use the name of the relation for the first entity |
|---|
| 539 | # (instead of the name of its primary key), so that we can |
|---|
| 540 | # have two many-to-many relations between the same objects |
|---|
| 541 | # without having a table name collision. |
|---|
| 542 | source_part = "%s_%s" % (e1_desc.tablename, self.name) |
|---|
| 543 | |
|---|
| 544 | # And we use only the name of the table of the second entity |
|---|
| 545 | # when there is no inverse, so that a many-to-many relation |
|---|
| 546 | # can be defined without an inverse. |
|---|
| 547 | if self.inverse: |
|---|
| 548 | target_part = "%s_%s" % (e2_desc.tablename, self.inverse.name) |
|---|
| 549 | else: |
|---|
| 550 | target_part = e2_desc.tablename |
|---|
| 551 | |
|---|
| 552 | if self.user_tablename: |
|---|
| 553 | tablename = self.user_tablename |
|---|
| 554 | else: |
|---|
| 555 | # We need to keep the table name consistent (independant of |
|---|
| 556 | # whether this relation or its inverse is setup first). |
|---|
| 557 | if self.inverse and e1_desc.tablename < e2_desc.tablename: |
|---|
| 558 | tablename = "%s__%s" % (target_part, source_part) |
|---|
| 559 | else: |
|---|
| 560 | tablename = "%s__%s" % (source_part, target_part) |
|---|
| 561 | |
|---|
| 562 | if e1_desc.autoload: |
|---|
| 563 | self._reflect_table(tablename) |
|---|
| 564 | else: |
|---|
| 565 | # We pre-compute the names of the foreign key constraints |
|---|
| 566 | # pointing to the source (local) entity's table and to the |
|---|
| 567 | # target's table |
|---|
| 568 | |
|---|
| 569 | # In some databases (at lease MySQL) the constraint names need |
|---|
| 570 | # to be unique for the whole database, instead of per table. |
|---|
| 571 | source_fk_name = "%s_fk" % source_part |
|---|
| 572 | if self.inverse: |
|---|
| 573 | target_fk_name = "%s_fk" % target_part |
|---|
| 574 | else: |
|---|
| 575 | target_fk_name = "%s_inverse_fk" % source_part |
|---|
| 576 | |
|---|
| 577 | columns = list() |
|---|
| 578 | constraints = list() |
|---|
| 579 | |
|---|
| 580 | joins = (self.primaryjoin_clauses, self.secondaryjoin_clauses) |
|---|
| 581 | for num, desc, fk_name in ((0, e1_desc, source_fk_name), |
|---|
| 582 | (1, e2_desc, target_fk_name)): |
|---|
| 583 | fk_colnames = list() |
|---|
| 584 | fk_refcols = list() |
|---|
| 585 | |
|---|
| 586 | for key in desc.primary_keys: |
|---|
| 587 | pk_col = key.column |
|---|
| 588 | |
|---|
| 589 | colname = '%s_%s' % (desc.tablename, pk_col.name) |
|---|
| 590 | |
|---|
| 591 | # In case we have a many-to-many self-reference, we |
|---|
| 592 | # need to tweak the names of the columns so that we |
|---|
| 593 | # don't end up with twice the same column name. |
|---|
| 594 | if self.entity is self.target: |
|---|
| 595 | colname += str(num + 1) |
|---|
| 596 | |
|---|
| 597 | col = Column(colname, pk_col.type) |
|---|
| 598 | columns.append(col) |
|---|
| 599 | |
|---|
| 600 | # Build the list of local columns which will be part |
|---|
| 601 | # of the foreign key. |
|---|
| 602 | fk_colnames.append(colname) |
|---|
| 603 | |
|---|
| 604 | # Build the list of columns the foreign key will point |
|---|
| 605 | # to. |
|---|
| 606 | fk_refcols.append(desc.tablename + '.' + pk_col.name) |
|---|
| 607 | |
|---|
| 608 | # Build join clauses (in case we have a self-ref) |
|---|
| 609 | if self.entity is self.target: |
|---|
| 610 | joins[num].append(col == pk_col) |
|---|
| 611 | |
|---|
| 612 | constraints.append( |
|---|
| 613 | ForeignKeyConstraint(fk_colnames, fk_refcols, |
|---|
| 614 | name=fk_name)) |
|---|
| 615 | |
|---|
| 616 | args = columns + constraints |
|---|
| 617 | |
|---|
| 618 | self.secondary_table = Table(tablename, e1_desc.metadata, |
|---|
| 619 | *args) |
|---|
| 620 | |
|---|
| 621 | def _reflect_table(self, tablename): |
|---|
| 622 | if not self.target._descriptor.autoload: |
|---|
| 623 | raise Exception( |
|---|
| 624 | "Entity '%s' is autoloaded and its '%s' " |
|---|
| 625 | "has_and_belongs_to_many relationship points to " |
|---|
| 626 | "the '%s' entity which is not autoloaded" |
|---|
| 627 | % (self.entity.__name__, self.name, |
|---|
| 628 | self.target.__name__)) |
|---|
| 629 | |
|---|
| 630 | self.secondary_table = Table(tablename, |
|---|
| 631 | self.entity._descriptor.metadata, |
|---|
| 632 | autoload=True) |
|---|
| 633 | |
|---|
| 634 | # In the case we have a self-reference, we need to build join clauses |
|---|
| 635 | if self.entity is self.target: |
|---|
| 636 | #CHECKME: maybe we should try even harder by checking if that |
|---|
| 637 | # information was defined on the inverse relationship) |
|---|
| 638 | if not self.local_side and not self.remote_side: |
|---|
| 639 | raise Exception( |
|---|
| 640 | "Self-referential has_and_belongs_to_many " |
|---|
| 641 | "relationships in autoloaded entities need to have at " |
|---|
| 642 | "least one of either 'local_side' or 'remote_side' " |
|---|
| 643 | "argument specified. The '%s' relationship in the '%s' " |
|---|
| 644 | "entity doesn't have either." |
|---|
| 645 | % (self.name, self.entity.__name__)) |
|---|
| 646 | |
|---|
| 647 | self.primaryjoin_clauses, self.secondaryjoin_clauses = \ |
|---|
| 648 | _build_join_clauses(self.secondary_table, |
|---|
| 649 | self.local_side, self.remote_side, |
|---|
| 650 | self.entity.table) |
|---|
| 651 | |
|---|
| 652 | def create_properties(self): |
|---|
| 653 | kwargs = self.kwargs |
|---|
| 654 | |
|---|
| 655 | if self.target is self.entity: |
|---|
| 656 | kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses) |
|---|
| 657 | kwargs['secondaryjoin'] = and_(*self.secondaryjoin_clauses) |
|---|
| 658 | |
|---|
| 659 | if 'order_by' in kwargs: |
|---|
| 660 | kwargs['order_by'] = \ |
|---|
| 661 | self.target._descriptor.translate_order_by(kwargs['order_by']) |
|---|
| 662 | |
|---|
| 663 | self.property = relation(self.target, secondary=self.secondary_table, |
|---|
| 664 | uselist=True, **kwargs) |
|---|
| 665 | self.entity.mapper.add_property(self.name, self.property) |
|---|
| 666 | |
|---|
| 667 | def is_inverse(self, other): |
|---|
| 668 | return super(HasAndBelongsToMany, self).is_inverse(other) and \ |
|---|
| 669 | (self.user_tablename == other.user_tablename or |
|---|
| 670 | (not self.user_tablename and not other.user_tablename)) |
|---|
| 671 | |
|---|
| 672 | |
|---|
| 673 | def _build_join_clauses(local_table, local_cols1, local_cols2, target_table): |
|---|
| 674 | primary_join, secondary_join = [], [] |
|---|
| 675 | cols1 = local_cols1[:] |
|---|
| 676 | cols1.sort() |
|---|
| 677 | cols1 = tuple(cols1) |
|---|
| 678 | |
|---|
| 679 | if local_cols2 is not None: |
|---|
| 680 | cols2 = local_cols2[:] |
|---|
| 681 | cols2.sort() |
|---|
| 682 | cols2 = tuple(cols2) |
|---|
| 683 | else: |
|---|
| 684 | cols2 = None |
|---|
| 685 | constraint_map = {} |
|---|
| 686 | for constraint in local_table.constraints: |
|---|
| 687 | if isinstance(constraint, ForeignKeyConstraint): |
|---|
| 688 | use_constraint = False |
|---|
| 689 | fk_colnames = [] |
|---|
| 690 | for fk in constraint.elements: |
|---|
| 691 | fk_colnames.append(fk.parent.name) |
|---|
| 692 | if fk.references(target_table): |
|---|
| 693 | use_constraint = True |
|---|
| 694 | if use_constraint: |
|---|
| 695 | fk_colnames.sort() |
|---|
| 696 | constraint_map[tuple(fk_colnames)] = constraint |
|---|
| 697 | |
|---|
| 698 | # Either the fk column names match explicitely with the columns given for |
|---|
| 699 | # one of the joins (primary or secondary), or we assume the current |
|---|
| 700 | # columns match because the columns for this join were not given and we |
|---|
| 701 | # know the other join is either not used (is None) or has an explicit |
|---|
| 702 | # match. |
|---|
| 703 | for cols, constraint in constraint_map.iteritems(): |
|---|
| 704 | if cols == cols1 or (cols != cols2 and |
|---|
| 705 | not cols1 and (cols2 in constraint_map or |
|---|
| 706 | cols2 is None)): |
|---|
| 707 | join = primary_join |
|---|
| 708 | elif cols == cols2 or (cols2 == () and cols1 in constraint_map): |
|---|
| 709 | join = secondary_join |
|---|
| 710 | else: |
|---|
| 711 | continue |
|---|
| 712 | for fk in constraint.elements: |
|---|
| 713 | join.append(fk.parent == fk.column) |
|---|
| 714 | return primary_join, secondary_join |
|---|
| 715 | |
|---|
| 716 | |
|---|
| 717 | belongs_to = Statement(BelongsTo) |
|---|
| 718 | has_one = Statement(HasOne) |
|---|
| 719 | has_many = Statement(HasMany) |
|---|
| 720 | has_and_belongs_to_many = Statement(HasAndBelongsToMany) |
|---|