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