summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy')
-rwxr-xr-xlib/sqlalchemy/ext/declarative.py276
1 files changed, 220 insertions, 56 deletions
diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py
index 7ff605146..d0a02381d 100755
--- a/lib/sqlalchemy/ext/declarative.py
+++ b/lib/sqlalchemy/ext/declarative.py
@@ -82,7 +82,7 @@ automatically::
Base = declarative_base(bind=create_engine('sqlite://'))
Alternatively, by way of the normal
-:class:`~sqlalchemy.schema.MetaData` behaviour, the ``bind`` attribute
+:class:`~sqlalchemy.schema.MetaData` behavior, the ``bind`` attribute
of the class level accessor can be assigned at any time as follows::
Base.metadata.bind = create_engine('sqlite://')
@@ -101,10 +101,7 @@ Configuring Relationships
Relationships to other classes are done in the usual way, with the added
feature that the class specified to :func:`~sqlalchemy.orm.relationship`
-may be a string name (note that :func:`~sqlalchemy.orm.relationship` is
-only available as of SQLAlchemy 0.6beta2, and in all prior versions is known
-as :func:`~sqlalchemy.orm.relation`,
-including 0.5 and 0.4). The "class registry" associated with ``Base``
+may be a string name. The "class registry" associated with ``Base``
is used at mapper compilation time to resolve the name into the actual
class object, which is expected to have been defined once the mapper
configuration is used::
@@ -421,74 +418,199 @@ requires usage of :func:`~sqlalchemy.orm.util.polymorphic_union`::
__mapper_args__ = {'polymorphic_identity':'manager', 'concrete':True}
-Mix-in Classes
+Mixin Classes
==============
A common need when using :mod:`~sqlalchemy.ext.declarative` is to
share some functionality, often a set of columns, across many
-classes. The normal python idiom would be to put this common code into
+classes. The normal Python idiom would be to put this common code into
a base class and have all the other classes subclass this class.
When using :mod:`~sqlalchemy.ext.declarative`, this need is met by
-using a "mix-in class". A mix-in class is one that isn't mapped to a
+using a "mixin class". A mixin class is one that isn't mapped to a
table and doesn't subclass the declarative :class:`Base`. For example::
class MyMixin(object):
- __table_args__ = {'mysql_engine':'InnoDB'}
- __mapper_args__=dict(always_refresh=True)
+ __table_args__ = {'mysql_engine': 'InnoDB'}
+ __mapper_args__= {'always_refresh': True}
+
id = Column(Integer, primary_key=True)
- def foo(self):
- return 'bar'+str(self.id)
class MyModel(Base,MyMixin):
- __tablename__='test'
- name = Column(String(1000), nullable=False, index=True)
+ __tablename__ = 'test'
-As the above example shows, ``__table_args__`` and ``__mapper_args__``
-can both be abstracted out into a mix-in if you use common values for
-these across many classes.
+ name = Column(String(1000))
-However, particularly in the case of ``__table_args__``, you may want
-to combine some parameters from several mix-ins with those you wish to
-define on the class iteself. To help with this, a
-:func:`~sqlalchemy.util.classproperty` decorator is provided that lets
-you implement a class property with a function. For example::
+Where above, the class ``MyModel`` will contain an "id" column
+as well as ``__table_args__`` and ``__mapper_args__`` defined
+by the ``MyMixin`` mixin class.
- from sqlalchemy.util import classproperty
+Mixing in Columns
+~~~~~~~~~~~~~~~~~
- class MySQLSettings:
- __table_args__ = {'mysql_engine':'InnoDB'}
+The most basic way to specify a column on a mixin is by simple
+declaration::
- class MyOtherMixin:
- __table_args__ = {'info':'foo'}
+ class TimestampMixin(object):
+ created_at = Column(DateTime, default=func.now())
- class MyModel(Base,MySQLSettings,MyOtherMixin):
- __tablename__='my_model'
+ class MyModel(Base, TimestampMixin):
+ __tablename__ = 'test'
+ id = Column(Integer, primary_key=True)
+ name = Column(String(1000))
+
+Where above, all declarative classes that include ``TimestampMixin``
+will also have a column ``created_at`` that applies a timestamp to
+all row insertions.
+
+Those familiar with the SQLAlchemy expression language know that
+the object identity of clause elements defines their role in a schema.
+Two ``Table`` objects ``a`` and ``b`` may both have a column called
+``id``, but the way these are differentiated is that ``a.c.id``
+and ``b.c.id`` are two distinct Python objects, referencing their
+parent tables ``a`` and ``b`` respectively.
+
+In the case of the mixin column, it seems that only one
+:class:`Column` object is explicitly created, yet the ultimate
+``created_at`` column above must exist as a distinct Python object
+for each separate destination class. To accomplish this, the declarative
+extension creates a **copy** of each :class:`Column` object encountered on
+a class that is detected as a mixin.
+
+This copy mechanism is limited to simple columns that have no foreign
+keys, as a :class:`ForeignKey` itself contains references to columns
+which can't be properly recreated at this level. For columns that
+have foreign keys, as well as for the variety of mapper-level constructs
+that require destination-explicit context, the
+:func:`~sqlalchemy.util.classproperty` decorator is provided so that
+patterns common to many classes can be defined as callables::
+
+ from sqlalchemy.util import classproperty
+
+ class ReferenceAddressMixin(object):
@classproperty
- def __table_args__(self):
- args = dict()
- args.update(MySQLSettings.__table_args__)
- args.update(MyOtherMixin.__table_args__)
- return args
+ def address_id(cls):
+ return Column(Integer, ForeignKey('address.id'))
+
+ class User(Base, ReferenceAddressMixin):
+ __tablename__ = 'user'
+ id = Column(Integer, primary_key=True)
+
+Where above, the ``address_id`` class-level callable is executed at the
+point at which the ``User`` class is constructed, and the declarative
+extension can use the resulting :class:`Column` object as returned by
+the method without the need to copy it.
+
+Columns generated by :func:`~sqlalchemy.util.classproperty` can also be
+referenced by ``__mapper_args__`` to a limited degree, currently
+by ``polymorphic_on`` and ``version_id_col``, by specifying the
+classdecorator itself into the dictionary - the declarative extension
+will resolve them at class construction time::
+
+ class MyMixin:
+ @classproperty
+ def type_(cls):
+ return Column(String(50))
+ __mapper_args__= {'polymorphic_on':type_}
+
+ class MyModel(Base,MyMixin):
+ __tablename__='test'
id = Column(Integer, primary_key=True)
+
+.. note:: The usage of :func:`~sqlalchemy.util.classproperty` with mixin
+ columns is a new feature as of SQLAlchemy 0.6.2.
+
+Mixing in Relationships
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Relationships created by :func:`~sqlalchemy.orm.relationship` are provided
+exclusively using the :func:`~sqlalchemy.util.classproperty` approach,
+eliminating any ambiguity which could arise when copying a relationship
+and its possibly column-bound contents. Below is an example which
+combines a foreign key column and a relationship so that two classes
+``Foo`` and ``Bar`` can both be configured to reference a common
+target class via many-to-one::
+
+ class RefTargetMixin(object):
+ @classproperty
+ def target_id(cls):
+ return Column('target_id', ForeignKey('target.id'))
+
+ @classproperty
+ def target(cls):
+ return relationship("Target")
+
+ class Foo(Base, RefTargetMixin):
+ __tablename__ = 'foo'
+ id = Column(Integer, primary_key=True)
+
+ class Bar(Base, RefTargetMixin):
+ __tablename__ = 'bar'
+ id = Column(Integer, primary_key=True)
+
+ class Target(Base):
+ __tablename__ = 'target'
+ id = Column(Integer, primary_key=True)
-Controlling table inheritance with mix-ins
+:func:`~sqlalchemy.orm.relationship` definitions which require explicit
+primaryjoin, order_by etc. expressions should use the string forms
+for these arguments, so that they are evaluated as late as possible.
+To reference the mixin class in these expressions, use the given ``cls``
+to get it's name::
+
+ class RefTargetMixin(object):
+ @classproperty
+ def target_id(cls):
+ return Column('target_id', ForeignKey('target.id'))
+
+ @classproperty
+ def target(cls):
+ return relationship("Target",
+ primaryjoin="Target.id==%s.target_id" % cls.__name__
+ )
+
+.. note:: The usage of :func:`~sqlalchemy.util.classproperty` with mixin
+ relationships is a new feature as of SQLAlchemy 0.6.2.
+
+
+Mixing in deferred(), column_property(), etc.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Like :func:`~sqlalchemy.orm.relationship`, all :class:`~sqlalchemy.orm.interfaces.MapperProperty`
+subclasses such as :func:`~sqlalchemy.orm.deferred`,
+:func:`~sqlalchemy.orm.column_property`, etc. ultimately involve references
+to columns, and therefore have the :func:`~sqlalchemy.util.classproperty` requirement so that no reliance on copying is needed::
+
+ class SomethingMixin(object):
+
+ @classproperty
+ def dprop(cls):
+ return deferred(Column(Integer))
+
+ class Something(Base, SomethingMixin):
+ __tablename__ = "something"
+
+.. note:: The usage of :func:`~sqlalchemy.util.classproperty` with mixin
+ mapper properties is a new feature as of SQLAlchemy 0.6.2.
+
+
+Controlling table inheritance with mixins
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``__tablename__`` attribute in conjunction with the hierarchy of
the classes involved controls what type of table inheritance, if any,
is configured by the declarative extension.
-If the ``__tablename__`` is computed by a mix-in, you may need to
+If the ``__tablename__`` is computed by a mixin, you may need to
control which classes get the computed attribute in order to get the
type of table inheritance you require.
-For example, if you had a mix-in that computes ``__tablename__`` but
-where you wanted to use that mix-in in a single table inheritance
+For example, if you had a mixin that computes ``__tablename__`` but
+where you wanted to use that mixin in a single table inheritance
hierarchy, you can explicitly specify ``__tablename__`` as ``None`` to
indicate that the class should not have a table mapped::
@@ -509,14 +631,14 @@ indicate that the class should not have a table mapped::
__mapper_args__ = {'polymorphic_identity': 'engineer'}
primary_language = Column(String(50))
-Alternatively, you can make the mix-in intelligent enough to only
+Alternatively, you can make the mixin intelligent enough to only
return a ``__tablename__`` in the event that no table is already
mapped in the inheritance hierarchy. To help with this, a
:func:`~sqlalchemy.ext.declarative.has_inherited_table` helper
function is provided that returns ``True`` if a parent class already
has a mapped table.
-As an examply, here's a mix-in that will only allow single table
+As an example, here's a mixin that will only allow single table
inheritance::
from sqlalchemy.util import classproperty
@@ -540,7 +662,7 @@ inheritance::
primary_language = Column(String(50))
If you want to use a similar pattern with a mix of single and joined
-table inheritance, you would need a slightly different mix-in and use
+table inheritance, you would need a slightly different mixin and use
it on any joined table child classes in addition to their parent
classes::
@@ -572,6 +694,36 @@ classes::
__mapper_args__ = {'polymorphic_identity': 'engineer'}
preferred_recreation = Column(String(50))
+Combining Table/Mapper Arguments from Multiple Mixins
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In the case of ``__table_args__`` or ``__mapper_args__``, you may want
+to combine some parameters from several mixins with those you wish to
+define on the class iteself. The
+:func:`~sqlalchemy.util.classproperty` decorator can be used here
+to create user-defined collation routines that pull from multiple
+collections::
+
+ from sqlalchemy.util import classproperty
+
+ class MySQLSettings:
+ __table_args__ = {'mysql_engine':'InnoDB'}
+
+ class MyOtherMixin:
+ __table_args__ = {'info':'foo'}
+
+ class MyModel(Base,MySQLSettings,MyOtherMixin):
+ __tablename__='my_model'
+
+ @classproperty
+ def __table_args__(self):
+ args = dict()
+ args.update(MySQLSettings.__table_args__)
+ args.update(MyOtherMixin.__table_args__)
+ return args
+
+ id = Column(Integer, primary_key=True)
+
Class Constructor
=================
@@ -639,10 +791,10 @@ def _as_declarative(cls, classname, dict_):
# dict_ will be a dictproxy, which we can't write to, and we need to!
dict_ = dict(dict_)
- column_copies = dict()
- potential_columns = dict()
+ column_copies = {}
+ potential_columns = {}
- mapper_args ={}
+ mapper_args = {}
table_args = inherited_table_args = None
tablename = None
parent_columns = ()
@@ -664,30 +816,42 @@ def _as_declarative(cls, classname, dict_):
if base is not cls:
inherited_table_args = True
elif base is not cls:
+ # we're a mixin.
+
if isinstance(obj, Column):
if obj.foreign_keys:
raise exceptions.InvalidRequestError(
- "Columns with foreign keys to other columns "
- "are not allowed on declarative mixins at this time."
- )
+ "Columns with foreign keys to other columns "
+ "must be declared as @classproperty callables "
+ "on declarative mixin classes. ")
if name not in dict_ and not (
- '__table__' in dict_ and name in dict_['__table__'].c
+ '__table__' in dict_ and
+ name in dict_['__table__'].c
):
- potential_columns[name] = column_copies[obj] = obj.copy()
- column_copies[obj]._creation_order = obj._creation_order
- elif isinstance(obj, RelationshipProperty):
+ potential_columns[name] = \
+ column_copies[obj] = \
+ obj.copy()
+ column_copies[obj]._creation_order = \
+ obj._creation_order
+ elif isinstance(obj, MapperProperty):
raise exceptions.InvalidRequestError(
- "relationships are not allowed on "
- "declarative mixins at this time.")
+ "Mapper properties (i.e. deferred,"
+ "column_property(), relationship(), etc.) must "
+ "be declared as @classproperty callables "
+ "on declarative mixin classes.")
+ elif isinstance(obj, util.classproperty):
+ dict_[name] = column_copies[obj] = getattr(cls, name)
+
# apply inherited columns as we should
for k, v in potential_columns.items():
if tablename or k not in parent_columns:
- dict_[k]=v
+ dict_[k] = v
+
if inherited_table_args and not tablename:
table_args = None
- # make sure that column copies are used rather than the original columns
- # from any mixins
+ # make sure that column copies are used rather
+ # than the original columns from any mixins
for k, v in mapper_args.iteritems():
mapper_args[k] = column_copies.get(v,v)