About
RSS

Bit Focus


对象炼金术 - SQLAlchemy 从外键到关系

Posted at 2012-03-12 08:31:04 | Updated at 2018-05-25 13:27:13

上节回顾, 使用下面这样的代码保存一个 Artist 与一个 Album, 两条 flush 令人感觉相当不适.
def save():
    artist = Artist(name='aki misawa')
    session = Session()
    try:
        session.add(artist)
        session.flush() # 0
        album = Album(name='stella musica', artist_id=artist.artist_id)
        session.add(album)
        session.flush() # 1
        session.commit()
    finally:
        session.close()
    单纯一个外键还不能很直观地解决对象之间的关联关系. 下面就引入 sqlalchemy.orm.relationship 来改善这块代码.
import sqlalchemy as sqla
import sqlalchemy.orm as sqlorm
from sqlalchemy.ext.declarative import declarative_base as sqla_declarative_base

Base = sqla_declarative_base()
# use MEMORY, not a database file.
# and disable SQL echo
engine = sqla.create_engine('sqlite:///:memory:', echo=False)

class Artist(Base):
    __tablename__ = 'artist'
    artist_id = sqla.Column('id', sqla.Integer, primary_key=True)
    name = sqla.Column('name', sqla.String)

class Album(Base):
    __tablename__ = 'album'
    album_id = sqla.Column('id', sqla.Integer, primary_key=True)
    name = sqla.Column('name', sqla.String)
    artist_id = sqla.Column('artist', sqla.ForeignKey('artist.id'))

    artist = sqlorm.relationship(Artist)
    在类 ArtistAlbum 定义完成后, 再用 relationshipAlbum 补上一个类成员, 表示它与 Artist 之间的关系.

    这样一来, SQLAlchemy 就能根据此 relationship, 从对象关系中产生外键值, 比如之前的 save 函数可以变成这个样子
Session = sqlorm.scoped_session(sqlorm.sessionmaker(bind=engine))

def save():
    artist = Artist(name='aki misawa')
    album = Album(name='stella musica', artist=artist)
    session = Session()
    try:
        session.add(album)
        session.flush()
        session.commit()
    finally:
        session.close()
并且, 此时只需要保存 album 即可, 不需要去理会 artist.

    来写个函数查一下数据表
def list_all():
    session = Session()
    try:
        print '= Artists ='
        for a in session.query(Artist).all():
            print a.name
            for album in a.albums:
                print '- Album:', album.name
        print ''
        print '= Albums ='
        for a in session.query(Album).all():
            print a.name
            print '+ Artist:', a.artist.name
    finally:
        session.close()

if __name__ == '__main__':
    save()
    list_all()
    这里使用到 session.query 来查询与数据表关联的类型对象, all() 调用表示全部取出.

    外键数据需要保持会话才能取得, 也就是说如果上述代码被修改成了
def list_all_albums():
    session = Session()
    try:
        return session.query(Album).all()
    finally:
        session.close()

if __name__ == '__main__':
    save()
    for a in list_all_albums():
        print a.name
        print '+ Artist:', a.artist.name # ERROR
则会在尝试取得 Album 对象的 artist 域时发生错误.

    现在可以从 Album 得到 Artist 对象, 但是反过来却不行. 那么照猫画虎, 再回头给 Artist 补个 relationship 试试.
class Artist(Base):
    __tablename__ = 'artist'
    artist_id = sqla.Column('id', sqla.Integer, primary_key=True)
    name = sqla.Column('name', sqla.String)
    albums = sqlorm.relationship(Album)

class Album(Base):
    __tablename__ = 'album'
    album_id = sqla.Column('id', sqla.Integer, primary_key=True)
    name = sqla.Column('name', sqla.String)
    artist_id = sqla.Column('artist', sqla.ForeignKey('artist.id'))
    artist = sqlorm.relationship(Artist)
于是乎就悲剧了, 因为 Artist 定义的时候, Album 还没有呢. 所以只能把这一行丢到后面去了, 如
class Artist(Base):
    __tablename__ = 'artist'
    artist_id = sqla.Column('id', sqla.Integer, primary_key=True)
    name = sqla.Column('name', sqla.String)
    # albums = sqlorm.relationship(Album)

class Album(Base):
    __tablename__ = 'album'
    album_id = sqla.Column('id', sqla.Integer, primary_key=True)
    name = sqla.Column('name', sqla.String)
    artist_id = sqla.Column('artist', sqla.ForeignKey('artist.id'))
    artist = sqlorm.relationship(Artist)

Artist.albums = sqlorm.relationship(Album) # put it here
    如果你跟我一样有偏好对称的审美观, 肯定会一不做二不休, 把 Album 里面的关系声明也给扒到外面去
class Artist(Base):
    __tablename__ = 'artist'
    artist_id = sqla.Column('id', sqla.Integer, primary_key=True)
    name = sqla.Column('name', sqla.String)

class Album(Base):
    __tablename__ = 'album'
    album_id = sqla.Column('id', sqla.Integer, primary_key=True)
    name = sqla.Column('name', sqla.String)
    artist_id = sqla.Column('artist', sqla.ForeignKey('artist.id'))
    # artist = sqlorm.relationship(Artist)

Artist.albums = sqlorm.relationship(Album)
Album.artist = sqlorm.relationship(Artist) # put it here
    差不多就这样了, 现在再来一发试试吧.
def save():
    artist0 = Artist(name='aki misawa')
    artist1 = Artist(name='katou emiri')
    album0 = Album(name='stella musica', artist=artist0)
    album1 = Album(name='hoshikage no ama no hara', artist=artist0)
    album2 = Album(name='jump!', artist=artist1)
    session = Session()
    try:
        session.add(album0)
        session.add(album1)
        session.add(album2)
        session.flush()
        session.commit()
    finally:
        session.close()

def list_all():
    session = Session()
    try:
        print '= Artists ='
        for a in session.query(Artist).all():
            print a.name
        print ''
        print '= Albums ='
        for a in session.query(Album).all():
            print a.name
            print '+ Artist:', a.artist.name
    finally:
        session.close()

if __name__ == '__main__':
    save()
    list_all()
    不过这样还是有问题, 当从 Artist 对象的 albums 删去个别数据时, 被删除的 Album 数据不会同步到数据库. 这时就需要引入更多配置了. 敬请关注下节: 关系的级联.

Post tags:   SQLAlchemy  Python  ORM  Tutorial

Leave a comment:




Creative Commons License Your comment will be licensed under
CC-NC-ND 3.0


. Back to Bit Focus
NijiPress - Copyright (C) Neuron Teckid @ Bit Focus
About this site