diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2013-11-29 18:53:53 -0500 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2013-11-29 18:53:53 -0500 |
commit | 58fab1be984a42400e67bdf7f15acd5a6d7c4b3c (patch) | |
tree | a31000a83412c349db1ac31bc9cd122c57a961a7 | |
parent | c368034ca058fad9454cd79f9912cda284cb50e6 (diff) | |
download | sqlalchemy-58fab1be984a42400e67bdf7f15acd5a6d7c4b3c.tar.gz |
- start reworking examples to include more code from the wiki.
- add the other versioning examples from the wiki
- modernize the dictlike examples
-rw-r--r-- | doc/build/orm/examples.rst | 115 | ||||
-rw-r--r-- | examples/versioned_history/__init__.py (renamed from examples/versioning/__init__.py) | 4 | ||||
-rw-r--r-- | examples/versioned_history/history_meta.py (renamed from examples/versioning/history_meta.py) | 0 | ||||
-rw-r--r-- | examples/versioned_history/test_versioning.py (renamed from examples/versioning/test_versioning.py) | 0 | ||||
-rw-r--r-- | examples/vertical/dictlike-polymorphic.py | 214 | ||||
-rw-r--r-- | examples/vertical/dictlike.py | 191 |
6 files changed, 224 insertions, 300 deletions
diff --git a/doc/build/orm/examples.rst b/doc/build/orm/examples.rst index 60d075a36..484865e7e 100644 --- a/doc/build/orm/examples.rst +++ b/doc/build/orm/examples.rst @@ -1,18 +1,21 @@ .. _examples_toplevel: -Examples -======== +============ +ORM Examples +============ The SQLAlchemy distribution includes a variety of code examples illustrating a select set of patterns, some typical and some not so typical. All are runnable and can be found in the ``/examples`` directory of the -distribution. Each example contains a README in its ``__init__.py`` file, -each of which are listed below. +distribution. Descriptions and source code for all can be found here. Additional SQLAlchemy examples, some user contributed, are available on the wiki at `<http://www.sqlalchemy.org/trac/wiki/UsageRecipes>`_. +Mapping Recipes +=============== + .. _examples_adjacencylist: Adjacency List @@ -27,80 +30,98 @@ Associations .. automodule:: examples.association - -.. _examples_instrumentation: - -Attribute Instrumentation -------------------------- - -.. automodule:: examples.custom_attributes - -.. _examples_caching: - -Dogpile Caching ---------------- - -.. automodule:: examples.dogpile_caching - Directed Graphs --------------- .. automodule:: examples.graphs Dynamic Relations as Dictionaries ----------------------------------- +------------------------------------ .. automodule:: examples.dynamic_dict .. _examples_generic_associations: Generic Associations --------------------- +------------------------ .. automodule:: examples.generic_associations -.. _examples_sharding: - -Horizontal Sharding -------------------- - -.. automodule:: examples.sharding - -Inheritance Mappings --------------------- - -.. automodule:: examples.inheritance - Large Collections ------------------ +------------------------ .. automodule:: examples.large_collection Nested Sets ------------ +------------ .. automodule:: examples.nested_sets -.. _examples_postgis: +.. _examples_xmlpersistence: -PostGIS Integration -------------------- +XML Persistence +------------------------ -.. automodule:: examples.postgis +.. automodule:: examples.elementtree + +Versioning Objects +------------------------ + +Versioning with a History Table +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: examples.versioned_history -Versioned Objects ------------------ +Versioning using Temporal Rows +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. automodule:: examples.versioning +.. automodule:: examples.versioned_rows Vertical Attribute Mapping --------------------------- +------------------------------------ .. automodule:: examples.vertical -.. _examples_xmlpersistence: -XML Persistence ---------------- +Inheritance Mapping Recipes +============================ + +Basic Inheritance Mappings +---------------------------------- + +.. automodule:: examples.inheritance + +Special APIs +============ + +.. _examples_instrumentation: + +Attribute Instrumentation +------------------------------------ + +.. automodule:: examples.custom_attributes + +.. _examples_sharding: + +Horizontal Sharding +------------------------ + +.. automodule:: examples.sharding + +Extending the ORM +================= + +.. _examples_caching: + +Dogpile Caching +------------------------ + +.. automodule:: examples.dogpile_caching + +.. _examples_postgis: + +PostGIS Integration +------------------------ + +.. automodule:: examples.postgis -.. automodule:: examples.elementtree diff --git a/examples/versioning/__init__.py b/examples/versioned_history/__init__.py index a2a6a1813..d630b994b 100644 --- a/examples/versioning/__init__.py +++ b/examples/versioned_history/__init__.py @@ -1,8 +1,6 @@ """ Illustrates an extension which creates version tables for entities and stores -records for each change. The same idea as Elixir's versioned extension, but -more efficient (uses attribute API to get history) and handles class -inheritance. The given extensions generate an anonymous "history" class which +records for each change. The given extensions generate an anonymous "history" class which represents historical versions of the target object. Usage is illustrated via a unit test module ``test_versioning.py``, which can diff --git a/examples/versioning/history_meta.py b/examples/versioned_history/history_meta.py index 8cb523434..8cb523434 100644 --- a/examples/versioning/history_meta.py +++ b/examples/versioned_history/history_meta.py diff --git a/examples/versioning/test_versioning.py b/examples/versioned_history/test_versioning.py index 906280555..906280555 100644 --- a/examples/versioning/test_versioning.py +++ b/examples/versioned_history/test_versioning.py diff --git a/examples/vertical/dictlike-polymorphic.py b/examples/vertical/dictlike-polymorphic.py index 872a7c52e..e3d5ba578 100644 --- a/examples/vertical/dictlike-polymorphic.py +++ b/examples/vertical/dictlike-polymorphic.py @@ -1,10 +1,7 @@ """Mapping a polymorphic-valued vertical table as a dictionary. -This example illustrates accessing and modifying a "vertical" (or -"properties", or pivoted) table via a dict-like interface. The 'dictlike.py' -example explains the basics of vertical tables and the general approach. This -example adds a twist- the vertical table holds several "value" columns, one -for each type of data that can be stored. For example:: +Builds upon the dictlike.py example to also add differently typed +columns to the "fact" table, e.g.:: Table('properties', metadata Column('owner_id', Integer, ForeignKey('owner.id'), @@ -25,80 +22,43 @@ we'll use a @hybrid_property to build a smart '.value' attribute that wraps up reading and writing those various '_value' columns and keeps the '.type' up to date. -Class decorators are used, so Python 2.6 or greater is required. """ from sqlalchemy.orm.interfaces import PropComparator -from sqlalchemy.orm import comparable_property from sqlalchemy.ext.hybrid import hybrid_property - -# Using the VerticalPropertyDictMixin from the base example -from .dictlike import VerticalPropertyDictMixin +from sqlalchemy import event +from sqlalchemy import literal_column +from .dictlike import ProxiedDictMixin class PolymorphicVerticalProperty(object): """A key/value pair with polymorphic value storage. - Supplies a smart 'value' attribute that provides convenient read/write - access to the row's current value without the caller needing to worry - about the 'type' attribute or multiple columns. - - The 'value' attribute can also be used for basic comparisons in queries, - allowing the row's logical value to be compared without foreknowledge of - which column it might be in. This is not going to be a very efficient - operation on the database side, but it is possible. If you're mapping to - an existing database and you have some rows with a value of str('1') and - others of int(1), then this could be useful. - - Subclasses must provide a 'type_map' class attribute with the following - form:: - - type_map = { - <python type> : ('type column value', 'column name'), - # ... - } - - For example,:: - - type_map = { - int: ('integer', 'integer_value'), - str: ('varchar', 'varchar_value'), - } + The class which is mapped should indicate typing information + within the "info" dictionary of mapped Column objects; see + the AnimalFact mapping below for an example. - Would indicate that a Python int value should be stored in the - 'integer_value' column and the .type set to 'integer'. Conversely, if the - value of '.type' is 'integer, then the 'integer_value' column is consulted - for the current value. """ - type_map = { - type(None): (None, None), - } - def __init__(self, key, value=None): self.key = key self.value = value @hybrid_property def value(self): - for discriminator, field in self.type_map.values(): - if self.type == discriminator: - return getattr(self, field) - return None + fieldname, discriminator = self.type_map[self.type] + if fieldname is None: + return None + else: + return getattr(self, fieldname) @value.setter def value(self, value): py_type = type(value) - if py_type not in self.type_map: - raise TypeError(py_type) - - for field_type in self.type_map: - discriminator, field = self.type_map[field_type] - field_value = None - if py_type == field_type: - self.type = discriminator - field_value = value - if field is not None: - setattr(self, field, field_value) + fieldname, discriminator = self.type_map[py_type] + + self.type = discriminator + if fieldname is not None: + setattr(self, fieldname, value) @value.deleter def value(self): @@ -113,9 +73,14 @@ class PolymorphicVerticalProperty(object): self.cls = cls def _case(self): - whens = [(text("'%s'" % p[0]), cast(getattr(self.cls, p[1]), String)) - for p in self.cls.type_map.values() - if p[1] is not None] + pairs = set(self.cls.type_map.values()) + whens = [ + ( + literal_column("'%s'" % discriminator), + cast(getattr(self.cls, attribute), String) + ) for attribute, discriminator in pairs + if attribute is not None + ] return case(whens, self.cls.type, null()) def __eq__(self, other): return self._case() == cast(other, String) @@ -125,69 +90,78 @@ class PolymorphicVerticalProperty(object): def __repr__(self): return '<%s %r=%r>' % (self.__class__.__name__, self.key, self.value) +@event.listens_for(PolymorphicVerticalProperty, "mapper_configured", propagate=True) +def on_new_class(mapper, cls_): + """Look for Column objects with type info in them, and work up + a lookup table.""" + + info_dict = {} + info_dict[type(None)] = (None, 'none') + info_dict['none'] = (None, 'none') + + for k in mapper.c.keys(): + col = mapper.c[k] + if 'type' in col.info: + python_type, discriminator = col.info['type'] + info_dict[python_type] = (k, discriminator) + info_dict[discriminator] = (k, discriminator) + cls_.type_map = info_dict if __name__ == '__main__': - from sqlalchemy import (MetaData, Table, Column, Integer, Unicode, - ForeignKey, UnicodeText, and_, not_, or_, String, Boolean, cast, text, + from sqlalchemy import (Column, Integer, Unicode, + ForeignKey, UnicodeText, and_, or_, String, Boolean, cast, null, case, create_engine) - from sqlalchemy.orm import mapper, relationship, Session + from sqlalchemy.orm import relationship, Session from sqlalchemy.orm.collections import attribute_mapped_collection + from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy.ext.associationproxy import association_proxy + + Base = declarative_base() - metadata = MetaData() - - animals = Table('animal', metadata, - Column('id', Integer, primary_key=True), - Column('name', Unicode(100))) - - chars = Table('facts', metadata, - Column('animal_id', Integer, ForeignKey('animal.id'), - primary_key=True), - Column('key', Unicode(64), primary_key=True), - Column('type', Unicode(16), default=None), - Column('int_value', Integer, default=None), - Column('char_value', UnicodeText, default=None), - Column('boolean_value', Boolean, default=None)) - - class AnimalFact(PolymorphicVerticalProperty): - type_map = { - int: ('integer', 'int_value'), - str: ('char', 'char_value'), - bool: ('boolean', 'boolean_value'), - type(None): (None, None), - } - - class Animal(VerticalPropertyDictMixin): - """An animal. - - Animal facts are available via the 'facts' property or by using - dict-like accessors on an Animal instance:: - - cat['color'] = 'calico' - # or, equivalently: - cat.facts['color'] = AnimalFact('color', 'calico') - """ - _property_type = AnimalFact - _property_mapping = 'facts' + class AnimalFact(PolymorphicVerticalProperty, Base): + """A fact about an animal.""" + + __tablename__ = 'animal_fact' + + animal_id = Column(ForeignKey('animal.id'), primary_key=True) + key = Column(Unicode(64), primary_key=True) + type = Column(Unicode(16)) + + # add information about storage for different types + # in the info dictionary of Columns + int_value = Column(Integer, info={'type': (int, 'integer')}) + char_value = Column(UnicodeText, info={'type': (str, 'string')}) + boolean_value = Column(Boolean, info={'type': (bool, 'boolean')}) + + class Animal(ProxiedDictMixin._base_class(Base)): + """an Animal""" + + __tablename__ = 'animal' + + id = Column(Integer, primary_key=True) + name = Column(Unicode(100)) + + facts = relationship("AnimalFact", + collection_class=attribute_mapped_collection('key')) + + _proxied = association_proxy("facts", "value", + creator= + lambda key, value: AnimalFact(key=key, value=value)) def __init__(self, name): self.name = name def __repr__(self): - return '<%s %r>' % (self.__class__.__name__, self.name) - + return "Animal(%r)" % self.name - mapper(Animal, animals, properties={ - 'facts': relationship( - AnimalFact, backref='animal', - collection_class=attribute_mapped_collection('key')), - }) - - mapper(AnimalFact, chars) + @classmethod + def with_characteristic(self, key, value): + return self.facts.any(key=key, value=value) engine = create_engine('sqlite://', echo=True) - metadata.create_all(engine) + Base.metadata.create_all(engine) session = Session(engine) stoat = Animal('stoat') @@ -227,30 +201,24 @@ if __name__ == '__main__': AnimalFact.value == True)))) print('weasel-like animals', q.all()) - # Save some typing by wrapping that up in a function: - with_characteristic = lambda key, value: and_(AnimalFact.key == key, - AnimalFact.value == value) - q = (session.query(Animal). - filter(Animal.facts.any( - with_characteristic('weasel-like', True)))) + filter(Animal.with_characteristic('weasel-like', True))) print('weasel-like animals again', q.all()) q = (session.query(Animal). - filter(Animal.facts.any(with_characteristic('poisonous', False)))) + filter(Animal.with_characteristic('poisonous', False))) print('animals with poisonous=False', q.all()) q = (session.query(Animal). - filter(or_(Animal.facts.any( - with_characteristic('poisonous', False)), - not_(Animal.facts.any(AnimalFact.key == 'poisonous'))))) + filter(or_( + Animal.with_characteristic('poisonous', False), + ~Animal.facts.any(AnimalFact.key == 'poisonous') + ) + ) + ) print('non-poisonous animals', q.all()) q = (session.query(Animal). filter(Animal.facts.any(AnimalFact.value == 5))) print('any animal with a .value of 5', q.all()) - # Facts can be queried as well. - q = (session.query(AnimalFact). - filter(with_characteristic('cuteness', 'very cute'))) - print(q.all()) diff --git a/examples/vertical/dictlike.py b/examples/vertical/dictlike.py index f17d1acc8..1a3631354 100644 --- a/examples/vertical/dictlike.py +++ b/examples/vertical/dictlike.py @@ -30,150 +30,101 @@ accessing them like a Python dict can be very convenient. The example below can be used with many common vertical schemas as-is or with minor adaptations. """ +from __future__ import unicode_literals +from sqlalchemy.util.compat import with_metaclass +from collections import MutableMapping +from abc import ABCMeta +from sqlalchemy.ext.declarative import DeclarativeMeta -class VerticalProperty(object): - """A key/value pair. - This class models rows in the vertical table. - """ - - def __init__(self, key, value): - self.key = key - self.value = value - - def __repr__(self): - return '<%s %r=%r>' % (self.__class__.__name__, self.key, self.value) - - -class VerticalPropertyDictMixin(object): +class ProxiedDictMixin(MutableMapping): """Adds obj[key] access to a mapped class. - This is a mixin class. It can be inherited from directly, or included - with multiple inheritence. - - Classes using this mixin must define two class properties:: - - _property_type: - The mapped type of the vertical key/value pair instances. Will be - invoked with two positional arugments: key, value - - _property_mapping: - A string, the name of the Python attribute holding a dict-based - relationship of _property_type instances. - - Using the VerticalProperty class above as an example,:: + This class basically proxies dictionary access to an attribute + called ``_proxied``. The class which inherits this class + should have an attribute called ``_proxied`` which points to a dictionary. - class MyObj(VerticalPropertyDictMixin): - _property_type = VerticalProperty - _property_mapping = 'props' - - mapper(MyObj, sometable, properties={ - 'props': relationship(VerticalProperty, - collection_class=attribute_mapped_collection('key'))}) - - Dict-like access to MyObj is proxied through to the 'props' relationship:: - - myobj['key'] = 'value' - # ...is shorthand for: - myobj.props['key'] = VerticalProperty('key', 'value') - - myobj['key'] = 'updated value'] - # ...is shorthand for: - myobj.props['key'].value = 'updated value' - - print myobj['key'] - # ...is shorthand for: - print myobj.props['key'].value + The use of this class is optional, as it requires some + elaborate metaclass arithmetic in order to use it with declarative. """ - _property_type = VerticalProperty - _property_mapping = None + def __len__(self): + return len(self._proxied) - __map = property(lambda self: getattr(self, self._property_mapping)) + def __iter__(self): + return iter(self._proxied) def __getitem__(self, key): - return self.__map[key].value - - def __setitem__(self, key, value): - property = self.__map.get(key, None) - if property is None: - self.__map[key] = self._property_type(key, value) - else: - property.value = value - - def __delitem__(self, key): - del self.__map[key] + return self._proxied[key] def __contains__(self, key): - return key in self.__map - - # Implement other dict methods to taste. Here are some examples: - def keys(self): - return self.__map.keys() + return key in self._proxied - def values(self): - return [prop.value for prop in self.__map.values()] + def __setitem__(self, key, value): + self._proxied[key] = value - def items(self): - return [(key, prop.value) for key, prop in self.__map.items()] + def __delitem__(self, key): + del self._proxied[key] - def __iter__(self): - return iter(self.keys()) + @classmethod + def _base_class(cls, base): + """Perform the requisite metaclass trickery in order + to get DeclarativeMeta and ABCMeta to play nicely together, + while also remaining Python 2/3 agnostic. + """ + return with_metaclass( + type(str("MutableBase"), (ABCMeta, DeclarativeMeta), {}), + base, cls) if __name__ == '__main__': - from sqlalchemy import (MetaData, Table, Column, Integer, Unicode, - ForeignKey, UnicodeText, and_, not_, create_engine) - from sqlalchemy.orm import mapper, relationship, Session + from sqlalchemy import (Column, Integer, Unicode, + ForeignKey, UnicodeText, and_, create_engine) + from sqlalchemy.orm import relationship, Session from sqlalchemy.orm.collections import attribute_mapped_collection + from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy.ext.associationproxy import association_proxy - metadata = MetaData() + Base = declarative_base() - # Here we have named animals, and a collection of facts about them. - animals = Table('animal', metadata, - Column('id', Integer, primary_key=True), - Column('name', Unicode(100))) + class AnimalFact(Base): + """A fact about an animal.""" - facts = Table('facts', metadata, - Column('animal_id', Integer, ForeignKey('animal.id'), - primary_key=True), - Column('key', Unicode(64), primary_key=True), - Column('value', UnicodeText, default=None),) + __tablename__ = 'animal_fact' - class AnimalFact(VerticalProperty): - """A fact about an animal.""" + animal_id = Column(ForeignKey('animal.id'), primary_key=True) + key = Column(Unicode(64), primary_key=True) + value = Column(UnicodeText) - class Animal(VerticalPropertyDictMixin): - """An animal. + class Animal(ProxiedDictMixin._base_class(Base)): + """an Animal""" - Animal facts are available via the 'facts' property or by using - dict-like accessors on an Animal instance:: + __tablename__ = 'animal' - cat['color'] = 'calico' - # or, equivalently: - cat.facts['color'] = AnimalFact('color', 'calico') - """ + id = Column(Integer, primary_key=True) + name = Column(Unicode(100)) - _property_type = AnimalFact - _property_mapping = 'facts' + facts = relationship("AnimalFact", + collection_class=attribute_mapped_collection('key')) + + _proxied = association_proxy("facts", "value", + creator= + lambda key, value: AnimalFact(key=key, value=value)) def __init__(self, name): self.name = name def __repr__(self): - return '<%s %r>' % (self.__class__.__name__, self.name) - + return "Animal(%r)" % self.name - mapper(Animal, animals, properties={ - 'facts': relationship( - AnimalFact, backref='animal', - collection_class=attribute_mapped_collection('key')), - }) - mapper(AnimalFact, facts) + @classmethod + def with_characteristic(self, key, value): + return self.facts.any(key=key, value=value) engine = create_engine("sqlite://") - metadata.create_all(engine) + Base.metadata.create_all(engine) + session = Session(bind=engine) stoat = Animal('stoat') @@ -194,9 +145,6 @@ if __name__ == '__main__': critter['cuteness'] = 'very' print('changing cuteness:') - engine.echo = True - session.commit() - engine.echo = False marten = Animal('marten') marten['color'] = 'brown' @@ -212,7 +160,6 @@ if __name__ == '__main__': loris['cuteness'] = 'fairly' loris['poisonous-part'] = 'elbows' session.add(loris) - session.commit() q = (session.query(Animal). filter(Animal.facts.any( @@ -220,27 +167,17 @@ if __name__ == '__main__': AnimalFact.value == 'reddish')))) print('reddish animals', q.all()) - # Save some typing by wrapping that up in a function: - with_characteristic = lambda key, value: and_(AnimalFact.key == key, - AnimalFact.value == value) - - q = (session.query(Animal). - filter(Animal.facts.any( - with_characteristic('color', 'brown')))) + q = session.query(Animal).\ + filter(Animal.with_characteristic("color", 'brown')) print('brown animals', q.all()) - q = (session.query(Animal). - filter(not_(Animal.facts.any( - with_characteristic('poisonous-part', 'elbows'))))) + q = session.query(Animal).\ + filter(~Animal.with_characteristic("poisonous-part", 'elbows')) print('animals without poisonous-part == elbows', q.all()) q = (session.query(Animal). - filter(Animal.facts.any(AnimalFact.value == 'somewhat'))) + filter(Animal.facts.any(value='somewhat'))) print('any animal with any .value of "somewhat"', q.all()) - # Facts can be queried as well. - q = (session.query(AnimalFact). - filter(with_characteristic('cuteness', 'very'))) - print('just the facts', q.all()) |