| 1 | ''' |
|---|
| 2 | This module provides support for defining the fields (columns) of your |
|---|
| 3 | entities. Elixir currently supports two syntaxes to do so: the default |
|---|
| 4 | `Attribute-based syntax`_ as well as the has_field_ DSL statement. |
|---|
| 5 | |
|---|
| 6 | Note that the old with_fields_ statement is currently deprecated in favor of |
|---|
| 7 | the `Attribute-based syntax`_. |
|---|
| 8 | |
|---|
| 9 | Attribute-based syntax |
|---|
| 10 | ---------------------- |
|---|
| 11 | |
|---|
| 12 | Here is a quick example of how to use the object-oriented syntax. |
|---|
| 13 | |
|---|
| 14 | .. sourcecode:: python |
|---|
| 15 | |
|---|
| 16 | class Person(Entity): |
|---|
| 17 | id = Field(Integer, primary_key=True) |
|---|
| 18 | name = Field(String(50), required=True) |
|---|
| 19 | biography = Field(Text) |
|---|
| 20 | photo = Field(Binary, deferred=True) |
|---|
| 21 | _email = Field(String(20), colname='email', synonym='email') |
|---|
| 22 | |
|---|
| 23 | def _set_email(self, email): |
|---|
| 24 | self._email = email |
|---|
| 25 | def _get_email(self): |
|---|
| 26 | return self._email |
|---|
| 27 | email = property(_get_email, _set_email) |
|---|
| 28 | |
|---|
| 29 | |
|---|
| 30 | The Field class takes one mandatory argument, which is its type. Please refer |
|---|
| 31 | to SQLAlchemy documentation for a list of `types supported by SQLAlchemy |
|---|
| 32 | <http://www.sqlalchemy.org/docs/04/types.html>`_. |
|---|
| 33 | |
|---|
| 34 | Following that first mandatory argument, fields can take any number of |
|---|
| 35 | optional keyword arguments. Please note that all the **arguments** that are |
|---|
| 36 | **not specifically processed by Elixir**, as mentioned in the documentation |
|---|
| 37 | below **are passed on to the SQLAlchemy ``Column`` object**. Please refer to |
|---|
| 38 | the `SQLAlchemy Column object's documentation |
|---|
| 39 | <http://www.sqlalchemy.org/docs/04/sqlalchemy_schema.html |
|---|
| 40 | #docstrings_sqlalchemy.schema_Column>`_ for more details about other |
|---|
| 41 | supported keyword arguments. |
|---|
| 42 | |
|---|
| 43 | The following Elixir-specific arguments are supported: |
|---|
| 44 | |
|---|
| 45 | +-------------------+---------------------------------------------------------+ |
|---|
| 46 | | Argument Name | Description | |
|---|
| 47 | +===================+=========================================================+ |
|---|
| 48 | | ``required`` | Specify whether or not this field can be set to None | |
|---|
| 49 | | | (left without a value). Defaults to ``False``, unless | |
|---|
| 50 | | | the field is a primary key. | |
|---|
| 51 | +-------------------+---------------------------------------------------------+ |
|---|
| 52 | | ``colname`` | Specify a custom name for the column of this field. By | |
|---|
| 53 | | | default the column will have the same name as the | |
|---|
| 54 | | | attribute. | |
|---|
| 55 | +-------------------+---------------------------------------------------------+ |
|---|
| 56 | | ``deferred`` | Specify whether this particular column should be | |
|---|
| 57 | | | fetched by default (along with the other columns) when | |
|---|
| 58 | | | an instance of the entity is fetched from the database | |
|---|
| 59 | | | or rather only later on when this particular column is | |
|---|
| 60 | | | first referenced. This can be useful when one wants to | |
|---|
| 61 | | | avoid loading a large text or binary field into memory | |
|---|
| 62 | | | when its not needed. Individual columns can be lazy | |
|---|
| 63 | | | loaded by themselves (by using ``deferred=True``) | |
|---|
| 64 | | | or placed into groups that lazy-load together (by using | |
|---|
| 65 | | | ``deferred`` = `"group_name"`). | |
|---|
| 66 | +-------------------+---------------------------------------------------------+ |
|---|
| 67 | | ``synonym`` | Specify a synonym name for this field. The field will | |
|---|
| 68 | | | also be usable under that name in keyword-based Query | |
|---|
| 69 | | | functions such as filter_by. The Synonym class (see the | |
|---|
| 70 | | | `properties` module) provides a similar functionality | |
|---|
| 71 | | | with an (arguably) nicer syntax, but a limited scope. | |
|---|
| 72 | +-------------------+---------------------------------------------------------+ |
|---|
| 73 | |
|---|
| 74 | has_field |
|---|
| 75 | --------- |
|---|
| 76 | |
|---|
| 77 | The `has_field` statement allows you to define fields one at a time. |
|---|
| 78 | |
|---|
| 79 | The first argument is the name of the field, the second is its type. Following |
|---|
| 80 | these, any number of keyword arguments can be specified for additional |
|---|
| 81 | behavior. The following arguments are supported: |
|---|
| 82 | |
|---|
| 83 | +-------------------+---------------------------------------------------------+ |
|---|
| 84 | | Argument Name | Description | |
|---|
| 85 | +===================+=========================================================+ |
|---|
| 86 | | ``through`` | Specify a relation name to go through. This field will | |
|---|
| 87 | | | not exist as a column on the database but will be a | |
|---|
| 88 | | | property which automatically proxy values to the | |
|---|
| 89 | | | ``attribute`` attribute of the object pointed to by the | |
|---|
| 90 | | | relation. If the ``attribute`` argument is not present, | |
|---|
| 91 | | | the name of the current field will be used. In an | |
|---|
| 92 | | | has_field statement, you can only proxy through a | |
|---|
| 93 | | | belongs_to or an has_one relationship. | |
|---|
| 94 | +-------------------+---------------------------------------------------------+ |
|---|
| 95 | | ``attribute`` | Name of the "endpoint" attribute to proxy to. This | |
|---|
| 96 | | | should only be used in combination with the ``through`` | |
|---|
| 97 | | | argument. | |
|---|
| 98 | +-------------------+---------------------------------------------------------+ |
|---|
| 99 | |
|---|
| 100 | |
|---|
| 101 | Here is a quick example of how to use ``has_field``. |
|---|
| 102 | |
|---|
| 103 | .. sourcecode:: python |
|---|
| 104 | |
|---|
| 105 | class Person(Entity): |
|---|
| 106 | has_field('id', Integer, primary_key=True) |
|---|
| 107 | has_field('name', String(50)) |
|---|
| 108 | |
|---|
| 109 | |
|---|
| 110 | with_fields |
|---|
| 111 | ----------- |
|---|
| 112 | The `with_fields` statement is **deprecated** in favor of the `attribute-based |
|---|
| 113 | syntax`_. |
|---|
| 114 | |
|---|
| 115 | It allows you to define all fields of an entity at once. |
|---|
| 116 | Each keyword argument to this statement represents one field, which should |
|---|
| 117 | be a `Field` object. The first argument to a Field object is its type. |
|---|
| 118 | Following it, any number of keyword arguments can be specified for |
|---|
| 119 | additional behavior. The `with_fields` statement supports the same keyword |
|---|
| 120 | arguments than the `has_field` statement. |
|---|
| 121 | |
|---|
| 122 | Here is a quick example of how to use ``with_fields``. |
|---|
| 123 | |
|---|
| 124 | .. sourcecode:: python |
|---|
| 125 | |
|---|
| 126 | class Person(Entity): |
|---|
| 127 | with_fields( |
|---|
| 128 | id = Field(Integer, primary_key=True), |
|---|
| 129 | name = Field(String(50)) |
|---|
| 130 | ) |
|---|
| 131 | ''' |
|---|
| 132 | import sys |
|---|
| 133 | |
|---|
| 134 | from sqlalchemy import Column |
|---|
| 135 | from sqlalchemy.orm import deferred, synonym |
|---|
| 136 | from sqlalchemy.ext.associationproxy import association_proxy |
|---|
| 137 | |
|---|
| 138 | from elixir.statements import ClassMutator |
|---|
| 139 | from elixir.properties import Property |
|---|
| 140 | |
|---|
| 141 | __doc_all__ = ['Field'] |
|---|
| 142 | |
|---|
| 143 | |
|---|
| 144 | class Field(Property): |
|---|
| 145 | ''' |
|---|
| 146 | Represents the definition of a 'field' on an entity. |
|---|
| 147 | |
|---|
| 148 | This class represents a column on the table where the entity is stored. |
|---|
| 149 | This object is only used with the `with_fields` syntax for defining all |
|---|
| 150 | fields for an entity at the same time. The `has_field` syntax does not |
|---|
| 151 | require the manual creation of this object. |
|---|
| 152 | ''' |
|---|
| 153 | |
|---|
| 154 | def __init__(self, type, *args, **kwargs): |
|---|
| 155 | super(Field, self).__init__() |
|---|
| 156 | |
|---|
| 157 | self.colname = kwargs.pop('colname', None) |
|---|
| 158 | self.synonym = kwargs.pop('synonym', None) |
|---|
| 159 | self.deferred = kwargs.pop('deferred', False) |
|---|
| 160 | if 'required' in kwargs: |
|---|
| 161 | kwargs['nullable'] = not kwargs.pop('required') |
|---|
| 162 | self.type = type |
|---|
| 163 | self.primary_key = kwargs.get('primary_key', False) |
|---|
| 164 | |
|---|
| 165 | self.column = None |
|---|
| 166 | self.property = None |
|---|
| 167 | |
|---|
| 168 | self.args = args |
|---|
| 169 | self.kwargs = kwargs |
|---|
| 170 | |
|---|
| 171 | def attach(self, entity, name): |
|---|
| 172 | # If no colname was defined (through the 'colname' kwarg), set |
|---|
| 173 | # it to the name of the attr. |
|---|
| 174 | if self.colname is None: |
|---|
| 175 | self.colname = name |
|---|
| 176 | super(Field, self).attach(entity, name) |
|---|
| 177 | |
|---|
| 178 | def create_pk_cols(self): |
|---|
| 179 | if self.primary_key: |
|---|
| 180 | self.create_col() |
|---|
| 181 | |
|---|
| 182 | def create_non_pk_cols(self): |
|---|
| 183 | if not self.primary_key: |
|---|
| 184 | self.create_col() |
|---|
| 185 | |
|---|
| 186 | def create_col(self): |
|---|
| 187 | self.column = Column(self.colname, self.type, |
|---|
| 188 | *self.args, **self.kwargs) |
|---|
| 189 | self.entity._descriptor.add_column(self.column) |
|---|
| 190 | |
|---|
| 191 | def create_properties(self): |
|---|
| 192 | if self.deferred: |
|---|
| 193 | group = None |
|---|
| 194 | if isinstance(self.deferred, basestring): |
|---|
| 195 | group = self.deferred |
|---|
| 196 | self.property = deferred(self.column, group=group) |
|---|
| 197 | elif self.name != self.colname: |
|---|
| 198 | # if the property name is different from the column name, we need to |
|---|
| 199 | # add an explicit property (otherwise nothing is needed as it's done |
|---|
| 200 | # automatically by SA) |
|---|
| 201 | self.property = self.column |
|---|
| 202 | |
|---|
| 203 | if self.property: |
|---|
| 204 | self.entity._descriptor.add_property(self.name, self.property) |
|---|
| 205 | |
|---|
| 206 | if self.synonym: |
|---|
| 207 | self.entity._descriptor.add_property(self.synonym, |
|---|
| 208 | synonym(self.name)) |
|---|
| 209 | |
|---|
| 210 | |
|---|
| 211 | def has_field_handler(entity, name, *args, **kwargs): |
|---|
| 212 | if 'through' in kwargs: |
|---|
| 213 | setattr(entity, name, |
|---|
| 214 | association_proxy(kwargs.pop('through'), |
|---|
| 215 | kwargs.pop('attribute', name), |
|---|
| 216 | **kwargs)) |
|---|
| 217 | return |
|---|
| 218 | field = Field(*args, **kwargs) |
|---|
| 219 | field.attach(entity, name) |
|---|
| 220 | |
|---|
| 221 | |
|---|
| 222 | def with_fields_handler(entity, *args, **fields): |
|---|
| 223 | for name, field in fields.iteritems(): |
|---|
| 224 | field.attach(entity, name) |
|---|
| 225 | |
|---|
| 226 | |
|---|
| 227 | has_field = ClassMutator(has_field_handler) |
|---|
| 228 | with_fields = ClassMutator(with_fields_handler) |
|---|