定义关系
在关系型数据库中,我们可以通过关系让不同表之间的字段建立联系。一般来说,定义关系需要两步,分别是创建外键和定义关系属性。在更复杂的多对多关系中,我们还需要定义关联表来管理关系。下面我们学习用SQLAlchemy在模型之间建立几种基础的关系模式。
配置python shell上下文
在上面的操作中,每一次使用flask shell命令启动python shell后都要从app模块里导入db对象和相应的模型类。为什么不能把他们自动集成到python shell上下文里呢?就像flask内置的app对象一样。这当然可以实现,我们可以使用app.shell_context_processor装饰器注册一个shell上下文处理函数。它和模板上下文处理函数一样,也需要返回包含变量和变量值字典。
app.py: 注册shell上下文处理函数
@app.shell_context_processor def make_shell_context(): return dict(db=db, Note=Note) # 等同于{'db': db, 'Note': Note}
当你使用flask shell命令启动python shell时,所有使用app.shell_context_processor装饰器注册的shell上下文处理函数都会被自动执行,这会将db和Note对象推送到python shell上下文里:
>>> db <SQLAlchemy engine=sqlite:///D:flaskFLASK_PRACTICEDataBasedata.db> >>> Note <class 'app.Note'>
在下面演示各种数据库关系时,将编写更多的模型类。在示例程序中,都使用shell上下文处理函数添加到shell上下文中,因此你可以直接在python shell使用,不用手动导入。
一对多
我们将以作者和文章来演示一对多关系:一个作者可以写多篇文章。一对多关系如下图:
在示例程序中,Author类用来表示作者,Article类用来表示文章。
app.py: 一对多关系示例
class Author(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(70), unique=True) class Article(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(50), index=True) body = db.Column(db.Text)
我们将在这两个模型之间建立一个一对对关系,建立这个一对多关系的目的是在表示作者的Author类中添加一个关系属性articles,作为集合(collection)属性,当我们对特定的Author对象调用articles属性会返回所有的Article对象。下面来介绍如何一步步定义这个一对多关系。
1、定义外键
定义关系的第一步是创建外键。外键是(foreign key)用来在A表存储B表的主键值以便和B表建立联系的关联字段。因为外键只能存储单一数据(标量),所以外键总是在“多”这一侧定义,多篇文章属于同一个作者,所以我们需要为每篇文章添加外键存储作者的主键值以指向对应的作者。在Article模型中,我们定义一个author_id字段作为外键:
class Article(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(50), index=True) body = db.Column(db.Text) author_id = db.Column(db.Integer, db.ForeignKey('authod.id')) #指定显示内容,否则默认显示<表名 主键id> def __repr__(self): return '<Article %r>' % self.title
这个字段使用db.ForeignKey类定义为外键,传入关系另一侧的表名和主键字段名,即author.id。实际的效果是将article表的authod_id的值限制为author表的id列的值。它将用来存储author表中记录的主键值,如下图:
外键字段的命名没有限制,因为要连接的目标字段是author表的id列,所以为了便于区别而将这个外键字段的名称命名为author_id。
传入ForeignKey类的参数author.id,其中author指的是Author模型对应的表名称,而id指的是字段名,即“表名.字段名”。模型类对应的表名由Flask-SQLAlchemy生成,默认为类名称的小写形式,多个单词通过下划线分隔,你也可以显示地通过__tablename__属性自己指定。
2、定义关系属性
定义关系的第二步是使用关系函数定义关系属性。关系属性在关系的出发侧定义,即一对多关系的“一”这一侧。一个作者拥有多篇文章,在Author模型中,定义一个articles属性来表示对应的多篇文章:
class Author(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(70), unique=True) articles = db.relationShip('Article') #指定显示内容 def __repr__(self): return '<Author %r>' %self.name
关系属性的名称没有限制,你可以自由修改。它相当于一个快捷查询,不会作为字段写入数据库中。
这个属性并没有使用column类声明为列,而是使用了db.relationship()关系函数定义为关系属性,因为这个关系属性返回多个记录,我们称之为集合关系属性。relationship()函数的第一个参数为关系另一侧的模型名称,它会告诉SQLAlchemy将Author类与Article类建立关系。当这个关系属性被调用时,SQLAlchemy会找到关系的另一侧(即article表)的外键字段(即author_id),然后反向查询article表中所有author_id值为当前表主键值(即author.id)的记录,返回包含这些记录的列表,也就是返回某个作者对应的多篇文章记录。
>>> from app import Author, Article >>> from app import db >>> foo = Author(name = 'Foo') >>> spam = Article(title = 'Spam') >>> ham = Article(title = 'Ham') >>> db.session.add(foo) >>> db.session.add(spam) >>> db.session.add(ham)
3、建立关系
建立关系有两种方式,第一种方式为外键字段赋值,比如:
>>> spam.author_id = 1 >>> db.session.commit()
我们将spam对象的author_id字段的值设为1,这会和id值为1的Author对象建立关系。提交数据库改动后,如果我们队id为1的foo对象调用articles关系属性,会看到spam对象包括在返回的Article对象列表中:
>>> foo=Author.query.first() >>> foo <Author u'F00'> >>> foo.articles [<Article u'Spam'>, <Article u'Ham'>]
另一种方式是通过操作关系属性,将关系属性付给实际的对象即可建立关系。集合关系属性可以像列表一样操作,调用append()方法来与一个Article对象建立关系:
>>> foo.articles [<Article u'Spam'>, <Article u'Ham'>] >>> A1 = Article.query.get(3) >>> A1 <Article u'A1'> >>> foo.articles.append(A1) >>> db.session.commit() >>> foo.articles [<Article u'Spam'>, <Article u'Ham'>, <Article u'A1'>]
我们也可以直接将关系属性赋值给一个包含Article对象的列表。
>>> foo.articles [<Article u'Spam'>, <Article u'Ham'>, <Article u'A1'>] >>> foo.articles = foo.articles[1:2] >>> foo.articles [<Article u'Ham'>]
和前面的第一种方式类似,为了让改动生效,我们需要调用db.session.commit()方法提交数据库会话。
建立关系后,存储外键的author_id字段会自动获得正确的值,而调用Author实例的关系属性articles时,会获得所有建立关系的Article对象:
>>> foo.id 2 >>> A1.author_id 2 >>> foo.articles [<Article u'Spam'>, <Article u'Ham'>, <Article u'A1'>]
和主键类似,外键字段有SQLAlchemy管理,我们不需要手动设置。当通过关系属性建立关系后,外键字段会自动获得正确的值。
和append()相对,对关系属性调用remove()方法可以与对应的Article对象接触关系:
>>> soo.articles [<Article u'Ham'>, <Article u'A1'>] >>> soo.articles.remove(A1) >>> soo.articles [<Article u'Ham'>] >>> db.session.commit() >>> soo.articles [<Article u'Ham'>]
你也可以使用pop()方法操作关系属性,它会与关系属性对应的列表的最后一个Article对象接触关系并返回改对象。
不要忘记在操作结束后需要调用commit()方法提交数据库会话,这样才可以把数据写入数据库。
是用关系函数定义的属性不是数据库字段,而是类似于特定的查询函数。当某个Article对象被删除时,在对应Author对象的articles属性调用时返回的列表也不会包含该对象。
在关系函数中,有很多参数可以用来设置调用关系属性进行查询时的具体行为。常用的SQLAlchemy关系函数参数如下所示:
当关系属性被调用时,关系函数会加载相应的记录,下表列出了控制关系记录加载方式的lazy参数的常用选项。
常用的SQLAlchemy关系记录加载方式(lazy参数可选值):
dynamic选项仅用于集合关系属性,不可用于多对一、一对一或是在关系函数中将uselist参数设为False的情况。
要避免使用dynamic来动态加载所有集合关系属性对应的记录,使用dynamic加载方式以为这每次操作关系都会执行一次SQL查询,这会造成潜在的性能问题。大多数情况下我们只需要使用默认值(select),只有在调用关系属性会返回大量记录,并且总是需要对关系属性返回的结果附加额外的查询时才需要动态加载(lazy=’dynamic’)。
4、建立双向关系
我们在Author类中定义了集合关系属性articles,用来获取某个作者拥有的多篇文章记录。在某些情况下,你也许希望能在Article类中定义一个类似的author关系属性,当被调用时返回对应的作者记录,这类返回单个值的关系属性被称为标量关系属性。而这种两侧都添加关系属性获取对方记录的关系我们称之为双向关系(bidirectional relationship)
双向关系并不是必须的,但在某些情况下会非常方便。双向关系的建立很简单,通过在关系的另一侧也创建一个relationship()函数,我们就可以在两个表之间建立双向关系。我们使用作家(Writer)和书(Book)的一对多关系来进行演示,建立双向关系后的Writer和Book类如下所示:
app.py: 基于一对多关系的双向关系
class Writer(db.Model): id = db.Column(db.Integer,primary_key=True) name = db.Column(db.String(70), unique = True) #back_populates, 定义双向关系 # back_populates参数的值需要设为关系另一侧的关系属性名 books = db.relationship('Book', back_populates='writer') def __repr__(self): return '<Writer %r>' % self.name class Book(db.Model): id = db.Column(db.Integer, primary_key = True) title = db.Column(db.String(50), index = True) writer_id = db.Column(db.Integer, db.ForeignKey('writer.id')) writer = db.relationship('Writer', back_populates = 'books') def __repr__(self): return '<Book %r>' % self.title
在“多”这一侧的Book(书)类中,我们新创建了一个writer关系属性,这是一个标量关系属性,调用它会获取对应的Writer(作者)记录;而在Writer(作者)类中的books属性则用来获取对应的多Book(书)记录。在关系函数中,我们使用back_populates参数来连接对方,back_populates参数的值需要设为关系另一侧的关系属性名。
我们先创建1个Writer和2个Book记录,并添加到数据库中:
>>> from app import Writer >>> from app import Book, db >>> king = Writer(name = 'Stephen KKing') >>> carrie = Book(title = 'Carrie') >>> it = Book(title = 'IT') >>> db.session.add(king) >>> db.session.add(carrie) >>> db.session.add(it) >>> db.session.commit() #当一个book对象修改了writer属性,对应的writer对象的books属性会跟着修改(删去),#修改后的writer属性对应的writer对象的books属性会增加 >>> king.books [<Book u'IT'>, <Book 'Marry'>] >>> job = Writer(name = 'job') >>> db.session.add(job) >>> db.session.commit() >>> job.id 2 >>> marry = Book(title = 'Marry') >>> marry.writer = job >>> marry.writer <Writer u'job'> >>> king.books [<Book u'IT'>] >>> job.books [<Book u'Marry'>]
设置双向关系后,除了通过集合属性books(多个)来操作关系,我们也可以使用标量属性writer来进行关系操作。比如,将一个Writer对象赋值给某个Book对象的writer属性,就会和这个Book对象建立关系:
>>> king <Writer u'Stephen KKing'> >>> carrie <Book u'Carrie'> >>> carrie.writer = king >>> carrie.writer <Writer u'Stephen KKing'> #carrie这条数据的关系writer指向king,同时king这条数据的books属性也指向了carrie,这是双向的 >>> king.books [<Book u'Carrie'>] >>> it <Book u'IT'> >>> it.writer = king #同上 >>> king.books [<Book u'Carrie'>, <Book u'IT'>]
相对的,将某个book的writer属性设为None,就会解除与对应Writer对象的关系:
>>> carrie.writer = None >>> king.books [<Book u'IT'>] >>> db.session.commit()
需要注意的是,我们只需要在关系的一侧操作关系。当为Book对象的writer属性赋值后,对应Writer对象的books属性的返回值也会自动包含这个Book对象。反之,当某个Writer对象被删除时,对应的Book对象的writer属性被调用时的返回值也会被置为空(即NULL,会返回None)。
其他关系模式建立双向关系的方式完全相同。
5、使用backref简化关系定义
在介绍关系函数的参数时,我们曾提到过,使用关系函数中的backref参数可以简化双向关系的定义。以一对多关系为例,backref参数用来自动为关系另一侧添加关系属性,作为反向引用(back reference),赋予的值会作为关系另一侧的关系属性名称。比如,我们在Author一侧的关系函数中将backref参数设为author,SQLAlchemy会自动为Article类添加一个author属性。为了避免和前面的实例命名冲突,我们使用歌手(Singer)和歌曲(Song)的一对多关系作为演示,分别创建Singer和Song类,如下所示:
class Singer(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(70), unique=True) songs = db.relationship('Song', backref='singer') def __repr__(self): return "<Singer %r>" % self.name class Song(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), index=True) singer_id = db.Column(db.Integer, db.ForeignKey('singer.id')) def __repr__(self): return '<Song %r>' % self.name
在定义集合属性songs的关系函数中,我们将backref参数设置singer,这会同时在Song类中添加一个singer标量属性。我们仅需要定义这一个关系函数,虽然singer是一个“看不见的关系属性”,但是使用上和定义两个关系函数并使用back_poplulates参数的效果完全相同。
需要注意的是,使用backref允许我们仅在关系一侧定义另一侧的关系属性,但是在某些情况下,我们希望可以对在关系另一侧的关系属性进行设置,这时就需要使用backref()函数。backref()函数接收第一个参数作为在关系另一侧添加的关系属性名,其他关键字参数会作为关系另一侧关系函数的参数传入。比如,我们要在关系另一侧“看不见的relationship()函数”中将uselist参数设为False,可以这样:
class Singer(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(70), unique=True) songs = relationship('Song', backref=backref('singer', uselist=False)) >>> from app import Singer >>> from app import Song >>> singer1 = Singer(name = 'Huyanbin') >>> song1 = Song(name = 'HongYan') >>> singer1 <Singer 'Huyanbin'> >>> song1 <Song 'HongYan'> >>> song1.singer = singer1 >>> song1.singer <Singer 'Huyanbin'> >>> singer1.songs [<Song 'HongYan'>] >>> song2 = Song(name = 'manman') >>> song2.singer = singer1 >>> song2 <Song 'manman'> >>> singer1.songs [<Song 'HongYan'>, <Song 'manman'>] #提交数据库会话,落表 >>> db.session.add(singer1) >>> db.session.add(song1) >>> db.session.add(song2) >>> singer1.id >>> db.session.commit() >>> singer1.id 2 >>> song1.id 1 >>> song2.id 2
尽管使用backref非常方便,但通常来说“显示好过隐式”,所以我们应该尽量使用back_populates定义双向关系。