Well, you dug into a really esoteric and complex example there. association_proxy is a much easier way to get around these cases where you want an object to act like a scalar, so here's that, without all that crazy boilerplate of the "vertical" example, which I'd avoid as it is really too complex. Your example seemed undecided about primary key style so I went with the composite version. Surrogate + composite can't be mixed in a single table (well it can, but its relationally incorrect. The key should be the smallest unit that identifies a row - http://en.wikipedia.org/wiki/Unique_key is a good top level read into various subjects regarding this).
from sqlalchemy import Integer, String, Column, create_engine, ForeignKey, ForeignKeyConstraint 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() class AnimalFact(Base): """key/value attribute whose value can be either a string or a list of strings""" __tablename__ = 'animalfacts' # use either surrogate PK id, or the composite animal_id/key - but # not both. id/animal_id/key all together is not a proper key. # Personally I'd go for "id" here, but here's the composite version. animal_id = Column(Integer, ForeignKey('animal.id'), primary_key=True) key = Column(String, primary_key=True) # data str_value = Column(String) _list_value = relationship('StringEntry') # proxy list strings list_proxy = association_proxy('_list_value', 'value') def __init__(self, key, value): self.key = key self.value = value @property def value(self): if self.str_value is not None: return self.str_value else: return self.list_proxy @value.setter def value(self, value): if isinstance(value, basestring): self.str_value = value elif isinstance(value, list): self.list_proxy = value else: assert False class Animal(Base): __tablename__ = 'animal' id = Column(Integer, primary_key=True) name = Column(String) _facts = relationship(AnimalFact, backref='animal', collection_class=attribute_mapped_collection('key')) facts = association_proxy('_facts', 'value') def __init__(self, name): self.name = name # dictionary interface around "facts". # I'd just use "animal.facts" here, but here's how to skip that. def __getitem__(self, key): return self.facts.__getitem__(key) def __setitem__(self, key, value): self.facts.__setitem__(key, value) def __delitem__(self, key): self.facts.__delitem__(key) def __contains__(self, key): return self.facts.__contains__(key) def keys(self): return self.facts.keys() class StringEntry(Base): __tablename__ = 'myvalue' id = Column(Integer, primary_key=True) animal_id = Column(Integer) key = Column(Integer) value = Column(String) # because AnimalFact has a composite PK, we need # a composite FK. __table_args__ = (ForeignKeyConstraint( ['key', 'animal_id'], ['animalfacts.key', 'animalfacts.animal_id']), ) def __init__(self, value): self.value = value engine = create_engine('sqlite://', echo=True) Base.metadata.create_all(engine) session = Session(engine) # create a new animal a = Animal('aardvark') a['eyes'] = ['left side', 'right side'] a['cute'] = 'sort of' session.add(a) session.commit() session.close() for animal in session.query(Animal): print animal.name, ",".join(["%s" % animal[key] for key in animal.keys()]) On Aug 28, 2012, at 2:43 PM, Jacob Biesinger wrote: >> Yes, this is the pattern I'm trying to describe though I've been thinking of >> these as "typed collections", smallish tables for handling a particular >> table's collections needs, which are all of a single type (so rows are >> either a collection or a FK to ONE other table). It feels very doable, >> given the many examples I've seen that don't go quite this far. If it's a >> common enough use case, why not have an example on the best way to do this? > > Uh, scroll up, I said, "which you'll find in the **examples** with the > distribution". start reading here: > http://docs.sqlalchemy.org/en/rel_0_7/orm/examples.html#examples-generic-associations > > > I'm sorry to be a pest here. I've read through all the examples and have > spent several days with the documentation (which is really excellent). I > think what I'm describing is distinct from these patterns. I'm looking for a > polymorphic collection type capable of reflecting on the column (at run-time) > and presenting a different ORM interface. Extending the polymorphic vertical > column example at http://www.sqlalchemy.org/trac/browser/examples/vertical > shows what I'm trying to do pretty well. Again, please forgive me if I'm just > being dense. > > # set up declarative tables > # from http://www.sqlalchemy.org/trac/browser/examples/vertical > from sqlalchemy_examples.vertical import dictlike_polymorphic as dictlike > > metadata = MetaData() > Base = declarative_base() > engine = create_engine('sqlite://', echo=True) > Base.metadata.bind = engine > session = Session(engine) > > > class AnimalFact(dictlike.PolymorphicVerticalProperty, Base): > """key/value attribute whose value can be either a string or a list of > strings""" > __tablename__ = 'animalfacts' > type_map = {str: ('string', 'str_value'), > list: ('list', 'list_value')} > id = Column(Integer, primary_key=True) > animal_id = Column(Integer, ForeignKey('animal.id'), primary_key=True) > key = Column(String, primary_key=True) > type = Column(String) > str_value = Column(String) > list_value = relationship('StringEntry') > > > class Animal(Base, dictlike.VerticalPropertyDictMixin): > __tablename__ = 'animal' > _property_type = AnimalFact > _property_mapping = 'facts' > > id = Column(Integer, primary_key=True) > name = Column(String) > facts = relationship(AnimalFact, backref='animal', > collection_class=attribute_mapped_collection('key')) > > def __init__(self, name): > self.name = name > > > class StringEntry(Base): > __tablename__ = 'myvalue' > id = Column(Integer, primary_key=True) > animalfacts_id = Column(Integer, ForeignKey('animalfacts.id')) > value = Column(String) > > def __init__(self, value): > self.value = value > > Base.metadata.create_all() > > > # create a new animal > a = Animal('aardvark') > > # create a new AnimalFact. This uses the list_value column, which is in turn > a one-to-many relationship to StringEntry > a['eyes'] = [StringEntry('left side'), StringEntry('right side')] # works > great > > # this should use the str_value column, which is a simple string > a['cute'] = 'sort of' # fails with message "TypeError: Incompatible > collection type: None is not list-like" > > > Thanks as always for any help you can give! I'll update the stackoverflow > question once we get this squared away. > > -- > You received this message because you are subscribed to the Google Groups > "sqlalchemy" group. > To post to this group, send email to sqlalchemy@googlegroups.com. > To unsubscribe from this group, send email to > sqlalchemy+unsubscr...@googlegroups.com. > For more options, visit this group at > http://groups.google.com/group/sqlalchemy?hl=en. -- You received this message because you are subscribed to the Google Groups "sqlalchemy" group. To post to this group, send email to sqlalchemy@googlegroups.com. To unsubscribe from this group, send email to sqlalchemy+unsubscr...@googlegroups.com. For more options, visit this group at http://groups.google.com/group/sqlalchemy?hl=en.