为SQLAlchemy多对多数据库设置关系/映射

时间:2010-02-23 07:59:50

标签: python orm sqlalchemy

我是SQLAlchemy和关系数据库的新手,我正在尝试为带注释的词典设置模型。我想支持可以在运行时添加或删除的单词的任意数量的键值注释。由于密钥的名称会有很多重复,我不想直接使用this solution,尽管代码类似。

我的设计包含单词对象和属性对象。单词和属性存储在单独的表中,并带有一个property_values表,用于链接两者。这是代码:

from sqlalchemy import Column, Integer, String, Table, create_engine
from sqlalchemy import MetaData, ForeignKey
from sqlalchemy.orm import relation, mapper, sessionmaker
from sqlalchemy.ext.declarative import declarative_base

engine = create_engine('sqlite:///test.db', echo=True)
meta = MetaData(bind=engine)

property_values = Table('property_values', meta,
    Column('word_id', Integer, ForeignKey('words.id')),
    Column('property_id', Integer, ForeignKey('properties.id')),
    Column('value', String(20))
)
words = Table('words', meta,
    Column('id', Integer, primary_key=True),
    Column('name', String(20)),
    Column('freq', Integer)
)
properties = Table('properties', meta,
    Column('id', Integer, primary_key=True),
    Column('name', String(20), nullable=False, unique=True)
)
meta.create_all()

class Word(object):
    def __init__(self, name, freq=1):
        self.name = name
        self.freq = freq

class Property(object):
    def __init__(self, name):
        self.name = name
mapper(Property, properties)  

现在我希望能够做到以下几点:

Session = sessionmaker(bind=engine)
s = Session()
word = Word('foo', 42)
word['bar'] = 'yes' # or word.bar = 'yes' ?
s.add(word)
s.commit()

理想情况下,这应该将1|foo|42添加到单词表中,将1|bar添加到属性表中,并将1|1|yes添加到property_values表中。但是,我没有正确的映射和关系来实现这一目标。我从阅读http://www.sqlalchemy.org/docs/05/mappers.html#association-pattern处的文档中了解到我想在这里使用关联代理或类似的东西,但语法对我来说并不清楚。我试验过这个:

mapper(Word, words, properties={
    'properties': relation(Property, secondary=property_values)
    })

但是这个映射器只填充外键值,我还需要填写其他值。非常感谢任何帮助。

4 个答案:

答案 0 :(得分:1)

very similar question有轻微的界面差异。但通过定义__getitem____setitem____delitem__方法可以轻松解决此问题。

答案 1 :(得分:1)

只需使用Dictionary-Based Collections mapping映射 - 开箱即用的问题解决方案。摘自链接:

from sqlalchemy.orm.collections import column_mapped_collection, attribute_mapped_collection, mapped_collection

mapper(Item, items_table, properties={
    # key by column
    'notes': relation(Note, collection_class=column_mapped_collection(notes_table.c.keyword)),
    # or named attribute
    'notes2': relation(Note, collection_class=attribute_mapped_collection('keyword')),
    # or any callable
    'notes3': relation(Note, collection_class=mapped_collection(lambda entity: entity.a + entity.b))
})

# ...
item = Item()
item.notes['color'] = Note('color', 'blue')
print item.notes['color']

或尝试Inserting data in Many to Many relationship in SQLAlchemy的解决方案。显然,你必须用list替换dict逻辑 让问题作者用associationproxy发布最终代码,他最后提到了这个代码。

答案 2 :(得分:1)

对布伦特的评论,上面:

您可以使用session.flush()代替commit()来获取模型实例上的idflush()将执行必要的SQL,但不会提交,因此您可以在以后需要时回滚。

答案 3 :(得分:0)

我最终将Denis和van的帖子结合在一起形成了解决方案:

from sqlalchemy import Column, Integer, String, Table, create_engine
from sqlalchemy import MetaData, ForeignKey
from sqlalchemy.orm import relation, mapper, sessionmaker
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.declarative import declarative_base

meta = MetaData()
Base = declarative_base(metadata=meta, name='Base')

class PropertyValue(Base):
    __tablename__ = 'property_values'
    WordID = Column(Integer, ForeignKey('words.id'), primary_key=True)
    PropID = Column(Integer, ForeignKey('properties.id'), primary_key=True)
    Value = Column(String(20))

def _property_for_name(prop_name):
    return s.query(Property).filter_by(name=prop_name).first()

def _create_propval(prop_name, prop_val):
    p = _property_for_name(prop_name)
    if not p:
        p = Property(prop_name)
        s.add(p)
        s.commit()
    return PropertyValue(PropID=p.id, Value=prop_val)

class Word(Base):
    __tablename__ = 'words'
    id = Column(Integer, primary_key=True)
    string = Column(String(20), nullable=False)
    freq = Column(Integer)
    _props = relation(PropertyValue, collection_class=attribute_mapped_collection('PropID'), cascade='all, delete-orphan')
    props = association_proxy('_props', 'Value', creator=_create_propval)

    def __init__(self, string, freq=1):
        self.string = string
        self.freq = freq

    def __getitem__(self, prop):
        p = _property_for_name(prop)
        if p:
            return self.props[p.id]
        else:
            return None

    def __setitem__(self, prop, val):
        self.props[prop] = val

    def __delitem__(self, prop):
        p = _property_for_name(prop)
        if p:
            del self.props[prop]

class Property(Base):
    __tablename__ = 'properties'
    id = Column(Integer, primary_key=True)
    name = Column(String(20), nullable=False, unique=True)

    def __init__(self, name):
        self.name = name

engine = create_engine('sqlite:///test.db', echo=False)
Session = sessionmaker(bind=engine)
s = Session()
meta.create_all(engine)

测试代码如下:

word = Word('foo', 42)
word['bar'] = "yes"
word['baz'] = "certainly"
s.add(word)

word2 = Word('quux', 20)
word2['bar'] = "nope"
word2['groink'] = "nope"
s.add(word2)
word2['groink'] = "uh-uh"
del word2['bar']

s.commit()

word = s.query(Word).filter_by(string="foo").first()
print word.freq, word['baz']
# prints 42 certainly

数据库的内容是:

$ sqlite3 test.db "select * from property_values"
1|2|certainly
1|1|yes
2|3|uh-uh
$ sqlite3 test.db "select * from words"
1|foo|42
2|quux|20
$ sqlite3 test.db "select * from properties"
1|bar
2|baz
3|groink