From: Mike Bayer Date: Mon, 10 Mar 2008 17:14:08 +0000 (+0000) Subject: - a new super-small "declarative" extension has been added, X-Git-Tag: rel_0_4_4~16 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=88a8cc0c9e78c1ad83649f7f49bd05fc9dc25dd4;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - a new super-small "declarative" extension has been added, which allows Table and mapper() configuration to take place inline underneath a class declaration. This extension differs from ActiveMapper and Elixir in that it does not redefine any SQLAlchemy semantics at all; literal Column, Table and relation() constructs are used to define the class behavior and table definition. --- diff --git a/CHANGES b/CHANGES index d905bbc6de..938e720e0a 100644 --- a/CHANGES +++ b/CHANGES @@ -124,6 +124,15 @@ CHANGES when used with the ORM, mutable-style equality/ copy-on-write techniques are used to test for changes. +- extensions + - a new super-small "declarative" extension has been added, + which allows Table and mapper() configuration to take place + inline underneath a class declaration. This extension differs + from ActiveMapper and Elixir in that it does not redefine + any SQLAlchemy semantics at all; literal Column, Table + and relation() constructs are used to define the class + behavior and table definition. + 0.4.3 ------ - sql diff --git a/doc/build/content/ormtutorial.txt b/doc/build/content/ormtutorial.txt index b20570d53c..260b805fba 100644 --- a/doc/build/content/ormtutorial.txt +++ b/doc/build/content/ormtutorial.txt @@ -95,6 +95,13 @@ The `mapper()` function creates a new `Mapper` object and stores it away for fut What was that last `id` attribute? That was placed there by the `Mapper`, to track the value of the `id` column in the `users_table`. Since our `User` doesn't exist in the database, its id is `None`. When we save the object, it will get populated automatically with its new id. +## Too Verbose ? There are alternatives + +Some users, upon seeing the full set of steps to map a class, which are to define a `Table`, define a class, and then define a `mapper()`, are too verbose and disjoint. Most popular object relational products use the so-called "active record" approach, where the table definition and its class mapping are all defined at once. With SQLAlchemy, there are two excellent alternatives to its usual configuration: + + * [Elixir](http://elixir.ematia.de/) is a "sister" product to SQLAlchemy, which is a full "declarative" layer built on top of SQLAlchemy. It has existed almost as long as SA itself and defines a rich featureset on top of SA's normal configuration, adding many new capabilities such as plugins, automatic generation of table and column names based on configurations, and an intuitive system of defining relations. + * [declarative](rel:plugins_declarative) is a "micro-declarative" plugin included with SQLAlchemy 0.4.4 and above. In contrast to Elixir, it maintains virtually the identical configuration outlined in this tutorial, except it allows the `Column`, `relation()`, and other constructs to be defined "inline" with the mapped class itself. + ## Creating a Session We're now ready to start talking to the database. The ORM's "handle" to the database is the `Session`. When we first set up the application, at the same level as our `create_engine()` statement, we define a second object called `Session` (or whatever you want to call it, `create_session`, etc.) which is configured by the `sessionmaker()` function. This function is configurational and need only be called once. diff --git a/doc/build/content/plugins.txt b/doc/build/content/plugins.txt index fee1852b30..ed3869c248 100644 --- a/doc/build/content/plugins.txt +++ b/doc/build/content/plugins.txt @@ -3,6 +3,156 @@ Plugins {@name=plugins} SQLAlchemy has a variety of extensions available which provide extra functionality to SA, either via explicit usage or by augmenting the core behavior. Several of these extensions are designed to work together. +### declarative + +**Author:** Mike Bayer
+**Version:** 0.4.4 or greater + +`declarative` intends to be a fully featured replacement for the very old `activemapper` extension. Its goal is to redefine the organization of class, `Table`, and `mapper()` constructs such that they can all be defined "at once" underneath a class declaration. Unlike `activemapper`, it does not redefine normal SQLAlchemy configurational semantics - regular `Column`, `relation()` and other schema or ORM constructs are used in almost all cases. + +`declarative` is a so-called "micro declarative layer"; it does not generate table or column names and requires almost as fully verbose a configuration as that of straight tables and mappers. As an alternative, the [Elixir](http://elixir.ematia.de/) project is a full community-supported declarative layer for SQLAlchemy, and is recommended for its active-record-like semantics, its convention-based configuration, and plugin capabilities. + +SQLAlchemy object-relational configuration involves the usage of Table, mapper(), and class objects to define the three areas of configuration. +declarative moves these three types of configuration underneath the individual mapped class. Regular SQLAlchemy schema and ORM constructs are used +in most cases: + + {python} + from sqlalchemy.ext.declarative import declarative_base, declared_synonym + + engine = create_engine('sqlite://') + Base = declarative_base(engine) + + class SomeClass(Base): + __tablename__ = 'some_table' + id = Column('id', Integer, primary_key=True) + name = Column('name', String(50)) + +Above, the `declarative_base` callable produces a new base class from which all mapped classes inherit from. When the class definition is +completed, a new `Table` and `mapper()` have been generated, accessible via the `__table__` and `__mapper__` attributes on the +`SomeClass` class. + +Attributes may be added to the class after its construction, and they will be added to the underlying `Table` and `mapper()` definitions as +appropriate: + + {python} + SomeClass.data = Column('data', Unicode) + SomeClass.related = relation(RelatedInfo) + +Classes which are mapped explicitly using `mapper()` can interact freely with declarative classes. The `declarative_base` base class contains a +`MetaData` object as well as a dictionary of all classes created against the base. So to access the above metadata and create tables we can say: + + {python} + Base.metadata.create_all() + +The `declarative_base` can also receive a pre-created `MetaData` object: + + {python} + mymetadata = MetaData() + Base = declarative_base(metadata=mymetadata) + +Relations to other classes are done in the usual way, with the added feature that the class specified to `relation()` 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: + + {python} + class User(Base): + __tablename__ = 'users' + + id = Column('id', Integer, primary_key=True) + name = Column('name', String(50)) + addresses = relation("Address", backref="user") + + class Address(Base): + __tablename__ = 'addresses' + + id = Column('id', Integer, primary_key=True) + email = Column('email', String(50)) + user_id = Column('user_id', Integer, ForeignKey('users.id')) + +Column constructs, since they are just that, are immediately usable, as below where we define a primary join condition on the `Address` class +using them: + + {python} + class Address(Base) + __tablename__ = 'addresses' + + id = Column('id', Integer, primary_key=True) + email = Column('email', String(50)) + user_id = Column('user_id', Integer, ForeignKey('users.id')) + user = relation(User, primaryjoin=user_id==User.id) + +Synonyms are one area where `declarative` needs to slightly change the usual SQLAlchemy configurational syntax. To define a getter/setter which +proxies to an underlying attribute, use `declared_synonym`: + + {python} + class MyClass(Base): + __tablename__ = 'sometable' + + _attr = Column('attr', String) + + def _get_attr(self): + return self._some_attr + def _set_attr(self, attr) + self._some_attr = attr + attr = declared_synonym(property(_get_attr, _set_attr), '_attr') + +The above synonym is then usable as an instance attribute as well as a class-level expression construct: + + {python} + x = MyClass() + x.attr = "some value" + session.query(MyClass).filter(MyClass.attr == 'some other value').all() + +As an alternative to `__tablename__`, a direct `Table` construct may be used: + + {python} + class MyClass(Base): + __table__ = Table('my_table', Base.metadata, + Column('id', Integer, primary_key=True), + Column('name', String(50)) + ) + +This is the preferred approach when using reflected tables, as below: + + {python} + class MyClass(Base): + __table__ = Table('my_table', Base.metadata, autoload=True) + +Mapper arguments are specified using the `__mapper_args__` class variable. Note that the column objects declared on the class are immediately +usable, as in this joined-table inheritance example: + + {python} + class Person(Base): + __tablename__ = 'people' + id = Column('id', Integer, primary_key=True) + discriminator = Column('type', String(50)) + __mapper_args__ = {'polymorphic_on':discriminator} + + class Engineer(Person): + __tablename__ = 'engineers' + __mapper_args__ = {'polymorphic_identity':'engineer'} + id = Column('id', Integer, ForeignKey('people.id'), primary_key=True) + primary_language = Column('primary_language', String(50)) + +For single-table inheritance, the `__tablename__` and `__table__` class variables are optional on a class when the class inherits from another +mapped class. + +As a convenience feature, the `declarative_base()` sets a default constructor on classes which takes keyword arguments, and assigns them to the +named attributes: + + {python} + e = Engineer(primary_language='python') + +Note that `declarative` has no integration built in with sessions, and is only intended as an optional syntax for the regular usage of mappers +and Table objects. A typical application setup using `scoped_session` might look like: + + {python} + engine = create_engine('postgres://scott:tiger@localhost/test') + Session = scoped_session(sessionmaker(transactional=True, autoflush=False, bind=engine)) + Base = declarative_base() + +Mapped instances then make usage of `Session` in the usual way. + ### associationproxy @@ -361,7 +511,7 @@ Full SqlSoup documentation is on the [SQLAlchemy Wiki](http://www.sqlalchemy.org ### Deprecated Extensions -A lot of our extensions are deprecated. But this is a good thing. Why ? Because all of them have been refined and focused, and rolled into the core of SQLAlchemy (or in the case of `ActiveMapper`, it's become **Elixir**). So they aren't removed, they've just graduated into fully integrated features. Below we describe a set of extensions which are present in 0.4 but are deprecated. +A lot of our extensions are deprecated. But this is a good thing. Why ? Because all of them have been refined and focused, and rolled into the core of SQLAlchemy. So they aren't removed, they've just graduated into fully integrated features. Below we describe a set of extensions which are present in 0.4 but are deprecated. #### SelectResults @@ -444,7 +594,7 @@ For docs on `assignmapper`, see the SQLAlchemy 0.3 documentation. **Author:** Jonathan LaCour -Please note that ActiveMapper has been deprecated in favor of [Elixir](http://elixir.ematia.de/), a more comprehensive solution to declarative mapping, of which Jonathan is a co-author. +Please note that ActiveMapper has been deprecated in favor of either [Elixir](http://elixir.ematia.de/), a comprehensive solution to declarative mapping, or [declarative](rel:plugins_declarative), a built in convenience tool which reorganizes `Table` and `mapper()` configuration. ActiveMapper is a so-called "declarative layer" which allows the construction of a class, a `Table`, and a `Mapper` all in one step: diff --git a/doc/build/gen_docstrings.py b/doc/build/gen_docstrings.py index d7c6b210f1..d9bad13841 100644 --- a/doc/build/gen_docstrings.py +++ b/doc/build/gen_docstrings.py @@ -12,6 +12,7 @@ import sqlalchemy.ext.orderinglist as orderinglist import sqlalchemy.ext.associationproxy as associationproxy import sqlalchemy.ext.assignmapper as assignmapper import sqlalchemy.ext.sqlsoup as sqlsoup +import sqlalchemy.ext.declarative as declarative def make_doc(obj, classes=None, functions=None, **kwargs): """generate a docstring.ObjectDoc structure for an individual module, list of classes, and list of functions.""" @@ -45,6 +46,7 @@ def make_all_docs(): make_doc(obj=orm.query, classes=[orm.query.Query]), make_doc(obj=orm.session, classes=[orm.session.Session, orm.session.SessionExtension]), make_doc(obj=orm.shard), + make_doc(obj=declarative), make_doc(obj=associationproxy, classes=[associationproxy.AssociationProxy]), make_doc(obj=orderinglist, classes=[orderinglist.OrderingList]), make_doc(obj=sqlsoup), diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py new file mode 100644 index 0000000000..ac315e7550 --- /dev/null +++ b/lib/sqlalchemy/ext/declarative.py @@ -0,0 +1,321 @@ +"""A simple declarative layer for SQLAlchemy ORM. + +SQLAlchemy object-relational configuration involves the usage of Table, +mapper(), and class objects to define the three areas of configuration. +declarative moves these three types of configuration underneath the +individual mapped class. Regular SQLAlchemy schema and ORM +constructs are used in most cases:: + + from sqlalchemy.ext.declarative import declarative_base, declared_synonym + + engine = create_engine('sqlite://') + Base = declarative_base(engine) + + class SomeClass(Base): + __tablename__ = 'some_table' + id = Column('id', Integer, primary_key=True) + name = Column('name', String(50)) + +Above, the ``declarative_base`` callable produces a new base class from which all +mapped classes inherit from. When the class definition is completed, a new +``Table`` and ``mapper()`` have been generated, accessible via the ``__table__`` +and ``__mapper__`` attributes on the ``SomeClass`` class. + +Attributes may be added to the class after its construction, and they will +be added to the underlying ``Table`` and ``mapper()`` definitions as appropriate:: + + SomeClass.data = Column('data', Unicode) + SomeClass.related = relation(RelatedInfo) + +Classes which are mapped explicitly using ``mapper()`` can interact freely with +declarative classes. The ``declarative_base`` base class contains a ``MetaData`` +object as well as a dictionary of all classes created against the base. +So to access the above metadata and create tables we can say:: + + Base.metadata.create_all() + +The ``declarative_base`` can also receive a pre-created ``MetaData`` object:: + + mymetadata = MetaData() + Base = declarative_base(metadata=mymetadata) + +Relations to other classes are done in the usual way, with the added feature +that the class specified to ``relation()`` 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:: + + class User(Base): + __tablename__ = 'users' + + id = Column('id', Integer, primary_key=True) + name = Column('name', String(50)) + addresses = relation("Address", backref="user") + + class Address(Base): + __tablename__ = 'addresses' + + id = Column('id', Integer, primary_key=True) + email = Column('email', String(50)) + user_id = Column('user_id', Integer, ForeignKey('users.id')) + +Column constructs, since they are just that, are immediately usable, as +below where we define a primary join condition on the ``Address`` class +using them:: + + class Address(Base) + __tablename__ = 'addresses' + + id = Column('id', Integer, primary_key=True) + email = Column('email', String(50)) + user_id = Column('user_id', Integer, ForeignKey('users.id')) + user = relation(User, primaryjoin=user_id==User.id) + +Synonyms are one area where ``declarative`` needs to slightly change the usual +SQLAlchemy configurational syntax. To define a getter/setter which proxies +to an underlying attribute, use ``declared_synonym``:: + + class MyClass(Base): + __tablename__ = 'sometable' + + _attr = Column('attr', String) + + def _get_attr(self): + return self._some_attr + def _set_attr(self, attr) + self._some_attr = attr + attr = declared_synonym(property(_get_attr, _set_attr), '_attr') + +The above synonym is then usable as an instance attribute as well as a class-level +expression construct:: + + x = MyClass() + x.attr = "some value" + session.query(MyClass).filter(MyClass.attr == 'some other value').all() + +As an alternative to ``__tablename__``, a direct ``Table`` construct may be used:: + + class MyClass(Base): + __table__ = Table('my_table', Base.metadata, + Column('id', Integer, primary_key=True), + Column('name', String(50)) + ) + +This is the preferred approach when using reflected tables, as below:: + + class MyClass(Base): + __table__ = Table('my_table', Base.metadata, autoload=True) + +Mapper arguments are specified using the ``__mapper_args__`` class variable. +Note that the column objects declared on the class are immediately usable, as +in this joined-table inheritance example:: + + class Person(Base): + __tablename__ = 'people' + id = Column('id', Integer, primary_key=True) + discriminator = Column('type', String(50)) + __mapper_args__ = {'polymorphic_on':discriminator} + + class Engineer(Person): + __tablename__ = 'engineers' + __mapper_args__ = {'polymorphic_identity':'engineer'} + id = Column('id', Integer, ForeignKey('people.id'), primary_key=True) + primary_language = Column('primary_language', String(50)) + +For single-table inheritance, the ``__tablename__`` and ``__table__`` class +variables are optional on a class when the class inherits from another mapped +class. + +As a convenience feature, the ``declarative_base()`` sets a default constructor +on classes which takes keyword arguments, and assigns them to the named attributes:: + + e = Engineer(primary_language='python') + +Note that ``declarative`` has no integration built in with sessions, and is only +intended as an optional syntax for the regular usage of mappers and Table objects. +A typical application setup using ``scoped_session`` might look like:: + + engine = create_engine('postgres://scott:tiger@localhost/test') + Session = scoped_session(sessionmaker(transactional=True, autoflush=False, bind=engine)) + Base = declarative_base() + +Mapped instances then make usage of ``Session`` in the usual way. + +""" +from sqlalchemy.schema import Table, SchemaItem, Column, MetaData +from sqlalchemy.orm import synonym as _orm_synonym, mapper +from sqlalchemy.orm.interfaces import MapperProperty +from sqlalchemy.orm.properties import PropertyLoader + +__all__ = ['declarative_base', 'declared_synonym'] + +class DeclarativeMeta(type): + def __init__(cls, classname, bases, dict_): + if '_decl_class_registry' in cls.__dict__: + return type.__init__(cls, classname, bases, dict_) + + cls._decl_class_registry[classname] = cls + our_stuff = {} + for k in dict_: + value = dict_[k] + if not isinstance(value, (Column, MapperProperty, declared_synonym)): + continue + if isinstance(value, declared_synonym): + value._setup(cls, k, our_stuff) + else: + prop = _deferred_relation(cls, value) + our_stuff[k] = prop + + table = None + if '__table__' not in cls.__dict__: + if '__tablename__' in cls.__dict__: + tablename = cls.__tablename__ + cls.__table__ = table = Table(tablename, cls.metadata, *[ + c for c in our_stuff.values() if isinstance(c, Column) + ]) + else: + table = cls.__table__ + + inherits = cls.__mro__[1] + inherits = cls._decl_class_registry.get(inherits.__name__, None) + mapper_args = getattr(cls, '__mapper_args__', {}) + + cls.__mapper__ = mapper(cls, table, inherits=inherits, properties=our_stuff, **mapper_args) + return type.__init__(cls, classname, bases, dict_) + + def __setattr__(cls, key, value): + if '__mapper__' in cls.__dict__: + if isinstance(value, Column): + cls.__table__.append_column(value) + cls.__mapper__.add_property(key, value) + elif isinstance(value, MapperProperty): + cls.__mapper__.add_property(key, _deferred_relation(cls, value)) + elif isinstance(value, declared_synonym): + value._setup(cls, key, None) + else: + type.__setattr__(cls, key, value) + else: + type.__setattr__(cls, key, value) + +def _deferred_relation(cls, prop): + if isinstance(prop, PropertyLoader) and isinstance(prop.argument, basestring): + arg = prop.argument + def return_cls(): + return cls._decl_class_registry[arg] + prop.argument = return_cls + + return prop + +class declared_synonym(object): + def __init__(self, prop, name, mapperprop=None): + self.prop = prop + self.name = name + self.mapperprop = mapperprop + + def _setup(self, cls, key, init_dict): + prop = self.mapperprop or getattr(cls, self.name) + prop = _deferred_relation(cls, prop) + setattr(cls, key, self.prop) + if init_dict is not None: + init_dict[self.name] = prop + init_dict[key] = _orm_synonym(self.name) + else: + setattr(cls, self.name, prop) + setattr(cls, key, _orm_synonym(self.name)) + + +def declarative_base(engine=None, metadata=None): + lcl_metadata = metadata or MetaData() + class Base(object): + __metaclass__ = DeclarativeMeta + metadata = lcl_metadata + if engine: + metadata.bind = engine + _decl_class_registry = {} + def __init__(self, **kwargs): + for k in kwargs: + setattr(self, k, kwargs[k]) + return Base + +if __name__ == '__main__': + # sample usage: + + from sqlalchemy import * + from sqlalchemy.orm import * + + # Base is created per-app (or per desired scope) + # and houses a MetaData, and optionally an engine + Base = declarative_base(create_engine('sqlite://', echo=False)) + + class User(Base): + __tablename__ = 'users' + + id = Column('id', Integer, primary_key=True) + name = Column('name', String(50)) + addresses = relation("Address", backref="user") + type = Column('type', String(50)) + + __mapper_args__ = dict(polymorphic_on=type, polymorphic_identity='user') + + class AdminUser(User): + __tablename__ = 'admin_users' + __mapper_args__ = dict(polymorphic_identity='adminuser') + + id = Column('id', Integer, ForeignKey('users.id'), primary_key=True) + supername = Column('supername', String(50)) + + class Address(Base): + __tablename__ = 'addresses' + + id = Column('id', Integer, primary_key=True) + user_id = Column('user_id', Integer, ForeignKey('users.id')) + _email = Column('email', String(50)) + + # illustrate a synonym + def _set_email(self, email): + self._email = email + def _get_email(self): + return self._email + email = declared_synonym(property(_get_email, _set_email), '_email') + + class Keyword(Base): + __tablename__ = 'keywords' + + id = Column('id', Integer, primary_key=True) + name = Column('name', String(50)) + + # m2m tables are just created as Table objects + user_keywords = Table('user_keywords', Base.metadata, + Column('user_id', Integer, ForeignKey('users.id')), + Column('keyword_id', Integer, ForeignKey('keywords.id')) + ) + + # test adding relations after the fact + User.keywords = relation(Keyword, secondary=user_keywords, backref='users') + + Base.metadata.create_all() + + sess = create_session() + + u1 = User(name='ed', + addresses = [Address(email='eds email')], + keywords = [Keyword(name='one'), Keyword(name='two')] + ) + + sess.save(u1) + + a1 = AdminUser(name='some admin', supername='root', addresses=[ + Address(email='admin email') + ], keywords=[]) + sess.save(a1) + + sess.flush() + + sess.clear() + + print sess.query(User).filter(User.name=='ed').all() + print sess.query(User).filter(User.addresses.any(Address.email.like('%ed%'))).all() + + for user in sess.query(User).with_polymorphic('*').all(): + print user, user.addresses, user.keywords + \ No newline at end of file diff --git a/test/ext/alltests.py b/test/ext/alltests.py index 6f74e3dbce..d5db4d01ed 100644 --- a/test/ext/alltests.py +++ b/test/ext/alltests.py @@ -4,6 +4,7 @@ import doctest, sys, unittest def suite(): unittest_modules = ['ext.activemapper', 'ext.assignmapper', + 'ext.declarative', 'ext.orderinglist', 'ext.associationproxy'] diff --git a/test/ext/declarative.py b/test/ext/declarative.py new file mode 100644 index 0000000000..73d2578852 --- /dev/null +++ b/test/ext/declarative.py @@ -0,0 +1,240 @@ +import testenv; testenv.configure_for_tests() + +from sqlalchemy import * +from sqlalchemy.orm import * +from sqlalchemy.ext.declarative import declarative_base, declared_synonym +from testlib.fixtures import Base as Fixture +from testlib import * + + +class DeclarativeTest(TestBase): + def setUp(self): + global Base + Base = declarative_base(testing.db) + + def tearDown(self): + Base.metadata.drop_all() + + def test_basic(self): + class User(Base, Fixture): + __tablename__ = 'users' + + id = Column('id', Integer, primary_key=True) + name = Column('name', String(50)) + addresses = relation("Address", backref="user") + + class Address(Base, Fixture): + __tablename__ = 'addresses' + + id = Column('id', Integer, primary_key=True) + email = Column('email', String(50)) + user_id = Column('user_id', Integer, ForeignKey('users.id')) + + Base.metadata.create_all() + + u1 = User(name='u1', addresses=[ + Address(email='one'), + Address(email='two'), + ]) + sess = create_session() + sess.save(u1) + sess.flush() + sess.clear() + + self.assertEquals(sess.query(User).all(), [User(name='u1', addresses=[ + Address(email='one'), + Address(email='two'), + ])]) + + a1 = sess.query(Address).filter(Address.email=='two').one() + self.assertEquals(a1, Address(email='two')) + self.assertEquals(a1.user, User(name='u1')) + + def test_expression(self): + class User(Base, Fixture): + __tablename__ = 'users' + + id = Column('id', Integer, primary_key=True) + name = Column('name', String(50)) + addresses = relation("Address", backref="user") + + class Address(Base, Fixture): + __tablename__ = 'addresses' + + id = Column('id', Integer, primary_key=True) + email = Column('email', String(50)) + user_id = Column('user_id', Integer, ForeignKey('users.id')) + + User.address_count = column_property(select([func.count(Address.id)]).where(Address.user_id==User.id).as_scalar()) + + Base.metadata.create_all() + + u1 = User(name='u1', addresses=[ + Address(email='one'), + Address(email='two'), + ]) + sess = create_session() + sess.save(u1) + sess.flush() + sess.clear() + + self.assertEquals(sess.query(User).all(), [User(name='u1', address_count=2, addresses=[ + Address(email='one'), + Address(email='two'), + ])]) + + def test_synonym_inline(self): + class User(Base, Fixture): + __tablename__ = 'users' + + id = Column('id', Integer, primary_key=True) + _name = Column('name', String(50)) + def _set_name(self, name): + self._name = "SOMENAME " + name + def _get_name(self): + return self._name + name = declared_synonym(property(_get_name, _set_name), '_name') + + Base.metadata.create_all() + + sess = create_session() + u1 = User(name='someuser') + assert u1.name == "SOMENAME someuser", u1.name + sess.save(u1) + sess.flush() + self.assertEquals(sess.query(User).filter(User.name=="SOMENAME someuser").one(), u1) + + def test_synonym_added(self): + class User(Base, Fixture): + __tablename__ = 'users' + + id = Column('id', Integer, primary_key=True) + _name = Column('name', String(50)) + def _set_name(self, name): + self._name = "SOMENAME " + name + def _get_name(self): + return self._name + name = property(_get_name, _set_name) + User.name = declared_synonym(User.name, '_name') + + Base.metadata.create_all() + + sess = create_session() + u1 = User(name='someuser') + assert u1.name == "SOMENAME someuser", u1.name + sess.save(u1) + sess.flush() + self.assertEquals(sess.query(User).filter(User.name=="SOMENAME someuser").one(), u1) + + def test_joined_inheritance(self): + class Company(Base, Fixture): + __tablename__ = 'companies' + id = Column('id', Integer, primary_key=True) + name = Column('name', String(50)) + employees = relation("Person") + + class Person(Base, Fixture): + __tablename__ = 'people' + id = Column('id', Integer, primary_key=True) + company_id = Column('company_id', Integer, ForeignKey('companies.id')) + name = Column('name', String(50)) + discriminator = Column('type', String(50)) + __mapper_args__ = {'polymorphic_on':discriminator} + + class Engineer(Person): + __tablename__ = 'engineers' + __mapper_args__ = {'polymorphic_identity':'engineer'} + id = Column('id', Integer, ForeignKey('people.id'), primary_key=True) + primary_language = Column('primary_language', String(50)) + + class Manager(Person): + __tablename__ = 'managers' + __mapper_args__ = {'polymorphic_identity':'manager'} + id = Column('id', Integer, ForeignKey('people.id'), primary_key=True) + golf_swing = Column('golf_swing', String(50)) + + Base.metadata.create_all() + + sess = create_session() + c1 = Company(name="MegaCorp, Inc.", employees=[ + Engineer(name="dilbert", primary_language="java"), + Engineer(name="wally", primary_language="c++"), + Manager(name="dogbert", golf_swing="fore!") + ]) + + c2 = Company(name="Elbonia, Inc.", employees=[ + Engineer(name="vlad", primary_language="cobol") + ]) + + sess.save(c1) + sess.save(c2) + sess.flush() + sess.clear() + + self.assertEquals(sess.query(Company).filter(Company.employees.of_type(Engineer).any(Engineer.primary_language=='cobol')).first(), c2) + + def test_single_inheritance(self): + class Company(Base, Fixture): + __tablename__ = 'companies' + id = Column('id', Integer, primary_key=True) + name = Column('name', String(50)) + employees = relation("Person") + + class Person(Base, Fixture): + __tablename__ = 'people' + id = Column('id', Integer, primary_key=True) + company_id = Column('company_id', Integer, ForeignKey('companies.id')) + name = Column('name', String(50)) + discriminator = Column('type', String(50)) + primary_language = Column('primary_language', String(50)) + golf_swing = Column('golf_swing', String(50)) + __mapper_args__ = {'polymorphic_on':discriminator} + + class Engineer(Person): + __mapper_args__ = {'polymorphic_identity':'engineer'} + + class Manager(Person): + __mapper_args__ = {'polymorphic_identity':'manager'} + + Base.metadata.create_all() + + sess = create_session() + c1 = Company(name="MegaCorp, Inc.", employees=[ + Engineer(name="dilbert", primary_language="java"), + Engineer(name="wally", primary_language="c++"), + Manager(name="dogbert", golf_swing="fore!") + ]) + + c2 = Company(name="Elbonia, Inc.", employees=[ + Engineer(name="vlad", primary_language="cobol") + ]) + + sess.save(c1) + sess.save(c2) + sess.flush() + sess.clear() + + self.assertEquals(sess.query(Person).filter(Engineer.primary_language=='cobol').first(), Engineer(name='vlad')) + self.assertEquals(sess.query(Company).filter(Company.employees.of_type(Engineer).any(Engineer.primary_language=='cobol')).first(), c2) + + def test_reflection(self): + meta = MetaData(testing.db) + t1 = Table('t1', meta, Column('id', String(50), primary_key=True), Column('data', String(50))) + meta.create_all() + try: + class MyObj(Base): + __table__ = Table('t1', Base.metadata, autoload=True) + + sess = create_session() + m = MyObj(id="someid", data="somedata") + sess.save(m) + sess.flush() + + assert t1.select().execute().fetchall() == [('someid', 'somedata')] + + finally: + meta.drop_all() + + +if __name__ == '__main__': + testing.main() \ No newline at end of file