diff options
-rw-r--r-- | lib/sqlalchemy/ext/declarative.py | 166 | ||||
-rw-r--r-- | test/ext/declarative.py | 60 |
2 files changed, 134 insertions, 92 deletions
diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py index 333b1eaa4..7f30a7374 100644 --- a/lib/sqlalchemy/ext/declarative.py +++ b/lib/sqlalchemy/ext/declarative.py @@ -2,62 +2,73 @@ 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:: +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 - + 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. +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. + +You may omit the names from the Column definitions. Declarative will fill +them in for you. + + class SomeClass(Base): + __tablename__ = 'some_table' + id = Column(Integer, primary_key=True) + name = Column(String(50)) Attributes may be added to the class after its construction, and they will -be added to the underlying ``Table`` and ``mapper()`` definitions as appropriate:: +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:: +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:: + +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:: +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)) + id = Column(Integer, primary_key=True) + name = Column(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')) + id = Column(Integer, primary_key=True) + email = Column(String(50)) + user_id = Column(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 @@ -66,39 +77,41 @@ 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')) + id = Column(Integer, primary_key=True) + email = Column(String(50)) + user_id = Column(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 ``synonym`` with the ``instruments`` argument:: +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 ``synonym`` with the ``instruments`` +argument:: 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 = synonym('_attr', instruments=property(_get_attr, _set_attr)) - -The above synonym is then usable as an instance attribute as well as a class-level -expression construct:: + +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:: + +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)) + Column(Integer, primary_key=True), + Column(String(50)) ) This is the preferred approach when using reflected tables, as below:: @@ -106,42 +119,44 @@ 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:: +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)) + id = Column(Integer, primary_key=True) + discriminator = Column(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)) - + id = Column(Integer, ForeignKey('people.id'), primary_key=True) + primary_language = Column(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. +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:: +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:: +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. +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, comparable_property from sqlalchemy.orm.interfaces import MapperProperty @@ -151,11 +166,12 @@ from sqlalchemy import util __all__ = ['declarative_base', 'synonym_for', 'comparable_using', '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 = util.OrderedDict() for k in dict_: @@ -170,7 +186,7 @@ class DeclarativeMeta(type): continue prop = _deferred_relation(cls, value) our_stuff[k] = prop - + table = None if '__table__' not in cls.__dict__: if '__tablename__' in cls.__dict__: @@ -182,28 +198,25 @@ class DeclarativeMeta(type): table_kw = {} cols = [] for key, c in our_stuff.iteritems(): - if not isinstance(c, Column): - continue - if c.key is None: - c.key = key - if c.name is None: - c.name = key - cols.append(c) + if isinstance(c, Column): + _undefer_column_name(key, c) + cols.append(c) cls.__table__ = table = Table(tablename, cls.metadata, *cols, **table_kw) 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): + _undefer_column_name(key, value) cls.__table__.append_column(value) cls.__mapper__.add_property(key, value) elif isinstance(value, MapperProperty): @@ -224,13 +237,13 @@ def _deferred_relation(cls, prop): def declared_synonym(prop, name): """deprecated. use synonym(name, instrument=prop).""" - + return _orm_synonym(name, instrument=prop) declared_synonym = util.deprecated(declared_synonym) def synonym_for(name, map_column=False): """Decorator, make a Python @property a query synonym for a column. - + A decorator version of [sqlalchemy.orm#synonym()]. The function being decoratred is the 'instrument', otherwise passes its arguments through to synonym(). @@ -242,7 +255,7 @@ def synonym_for(name, map_column=False): The regular ``synonym()`` is also usable directly in a declarative setting and may be convenient for read/write properties:: - + prop = synonym('col', instrument=property(_read_prop, _write_prop)) """ @@ -287,3 +300,8 @@ def declarative_base(engine=None, metadata=None): setattr(self, k, kwargs[k]) return Base +def _undefer_column_name(key, column): + if column.key is None: + column.key = key + if column.name is None: + column.name = key diff --git a/test/ext/declarative.py b/test/ext/declarative.py index 99686dbe7..44f34cb44 100644 --- a/test/ext/declarative.py +++ b/test/ext/declarative.py @@ -13,10 +13,10 @@ 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' @@ -27,12 +27,12 @@ class DeclarativeTest(TestBase): class Address(Base, Fixture): __tablename__ = 'addresses' - + id = Column(Integer, primary_key=True) email = Column(String(50), key='_email') user_id = Column('user_id', Integer, ForeignKey('users.id'), key='_user_id') - + Base.metadata.create_all() assert Address.__table__.c['id'].name == 'id' @@ -47,12 +47,12 @@ class DeclarativeTest(TestBase): 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')) @@ -79,7 +79,7 @@ class DeclarativeTest(TestBase): id = Column('id', Integer, primary_key=True) name = Column('name', String(50)) addresses = relation("Address", backref="user") - + class Address(Base, Fixture): __tablename__ = 'addresses' @@ -105,6 +105,30 @@ class DeclarativeTest(TestBase): Address(email='two'), ])]) + def test_column(self): + class User(Base, Fixture): + __tablename__ = 'users' + + id = Column('id', Integer, primary_key=True) + name = Column('name', String(50)) + + User.a = Column('a', String(10)) + User.b = Column(String(10)) + + Base.metadata.create_all() + + print User.a + print User.c + + u1 = User(name='u1', a='a', b='b') + sess = create_session() + sess.save(u1) + sess.flush() + sess.clear() + + self.assertEquals(sess.query(User).all(), + [User(name='u1', a='a', b='b')]) + def test_synonym_inline(self): class User(Base, Fixture): __tablename__ = 'users' @@ -116,9 +140,9 @@ class DeclarativeTest(TestBase): def _get_name(self): return self._name name = synonym('_name', instrument=property(_get_name, _set_name)) - + Base.metadata.create_all() - + sess = create_session() u1 = User(name='someuser') assert u1.name == "SOMENAME someuser", u1.name @@ -192,14 +216,14 @@ class DeclarativeTest(TestBase): 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) @@ -207,7 +231,7 @@ class DeclarativeTest(TestBase): 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'} @@ -219,7 +243,7 @@ class DeclarativeTest(TestBase): __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() @@ -237,7 +261,7 @@ class DeclarativeTest(TestBase): 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_relation_reference(self): @@ -280,7 +304,7 @@ class DeclarativeTest(TestBase): 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) @@ -290,13 +314,13 @@ class DeclarativeTest(TestBase): 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() @@ -314,7 +338,7 @@ class DeclarativeTest(TestBase): 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) |