diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2008-03-10 17:14:08 +0000 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2008-03-10 17:14:08 +0000 |
commit | 88a8cc0c9e78c1ad83649f7f49bd05fc9dc25dd4 (patch) | |
tree | 688e7874698f43fa93b848b8e3fc9230fbc09b32 /lib/sqlalchemy/ext/declarative.py | |
parent | 9d3216d3f306f5b1be71bd164b60b9ef667a528e (diff) | |
download | sqlalchemy-88a8cc0c9e78c1ad83649f7f49bd05fc9dc25dd4.tar.gz |
- 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.
Diffstat (limited to 'lib/sqlalchemy/ext/declarative.py')
-rw-r--r-- | lib/sqlalchemy/ext/declarative.py | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py new file mode 100644 index 000000000..ac315e755 --- /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 |