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

Python 为SQLAlchemy多对多数据库设置关系/映射,python,orm,sqlalchemy,Python,Orm,Sqlalchemy,我不熟悉SQLAlchemy和关系数据库,我正在尝试为带注释的词典建立一个模型。我希望支持任意数量的关键字值注释,这些注释可以在运行时添加或删除。由于键的名称会有很多重复,我不想直接使用,尽管代码类似 我的设计有word对象和property对象。单词和属性存储在单独的表中,带有链接这两个单词和属性的属性值表。代码如下: from sqlalchemy import Column, Integer, String, Table, create_engine from sqlalchemy imp

我不熟悉SQLAlchemy和关系数据库,我正在尝试为带注释的词典建立一个模型。我希望支持任意数量的关键字值注释,这些注释可以在运行时添加或删除。由于键的名称会有很多重复,我不想直接使用,尽管代码类似

我的设计有word对象和property对象。单词和属性存储在单独的表中,带有链接这两个单词和属性的属性值表。代码如下:

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
添加到属性值表中。但是,我没有合适的映射和关系来实现这一点。我在阅读文档时感觉到,我想在这里使用关联代理或类似的东西,但语法对我来说并不清楚。我尝试过这个:

mapper(Word, words, properties={
    'properties': relation(Property, secondary=property_values)
    })
但是这个映射器只填充外键值,我还需要填充其他值。任何帮助都将不胜感激。

有一个接口略有不同。但是,通过定义
\uu getitem\uuuuuuuuu
\uuuuu setitem\uuuuuuu
\uuuuuu delitem\uuuuuuuu
方法,很容易解决这个问题。

只需使用现成的映射解决方案即可解决问题。从链接中摘录:

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']

或者尝试解决问题的方法。显然,您必须将
列表
逻辑替换为
dict
逻辑。

请问题作者用
associationproxy
发布他最后的代码,他最后提到了他使用的。

我最终将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

布伦特的评论,见上文:


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

感谢您提供的指针。我有一些类似的东西,但是我想将键名作为外键来维护,因为键中会有很多重复,我不想在数据库中复制所有这些字符串。我正在考虑将你的解决方案与上面van的解决方案相结合[.这是最简单的方法,但我的许多条目都有相同的键,因此我想对这些键进行唯一化,并将它们存储在单独的表中。我正在查看您发布的第二个解决方案。这很公平。但是,是什么阻止您使用基于字典的集合映射到对象上的属性,但除此之外,还只提供类似dict的属性对象上的接口,它基本上是这个属性的代理(委托)?我认为这基本上就是我在解决方案中提出的(发布在下面).在_create_propval中,你真的需要添加/提交新的属性对象吗?当你稍后提交会话时,它不会自动添加/提交吗?如果是,那么我不会在那里添加/提交,因为你可能会回滚会话而不是提交。是的,这是一个粗略的代码部分。我需要属性对象来获取id,所以创建PropertyValue时我可以引用它(因为PropertyValue使用外键来引用属性名称)。属性在提交之前不会获取其id。有没有不提交就获取id的好方法?对于记录,解决方案是将_create_propval中的s.commit()行替换为s.merge(p)。