使用SQLAlchemy建立模型之间的基础关系模式
class Author(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(70), unique=True) phone = db.Column(db.String(20)) 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)
定义外键:定义关系的第一步是创建外键。外键是(foreign key
)用来在 A 表存储 B 表的主键值以便和 B 表建立联系的关系字段。
因为外键只能存储单一数据(标量),所以外键总是在 “多” 这一侧定义,多篇文章属于同 一个作者,所以我们需要为每篇文章添加外键存储作者的主键值以指向 对应的作者。在 Article 模型中,我们定义一个 author_id
字段作为外键:
class Article(db.Model): ... author_id = db.Column(db.Integer, db.ForeignKey('author.id'))
这个字段使用 db.ForeignKey
类定义为外键,传入关系另一侧的表名 和主键字段名,即 author.id
。实际的效果是将 article 表的 author_id
的值限制为 author 表的 id 列的值。它将用来存储 author 表中记录的主键值。
定义关系的第二步是使用关系函数定义关系属性。关系属性在关系 的出发侧定义,即一对多关系的 “一” 这一侧。一个作者拥有多篇文章, 在 Author 模型中,我们定义了一个 articles 属性来表示对应的多篇文章:
class Author(db.Model): ... articles = db.relationship('Article')
这个属性并没有使用 Column 类声明为列,而是使用了 db.relationship()
关系函数定义为关系属性,因为这个关系属性返回多个记录,我们称之为集合关系属性。
relationship()
函数的第一个参数 为关系另一侧的模型名称,它会告诉 SQLAlchemy 将 Author
类与 Article
类建立关系。当这个关系属性被调用时,SQLAlchemy 会找到关系另一侧 (即 article
表)的外键字段(即 author_id
),然后反向查询 article
表中所有 author_id
值为当前表主键值(即 author.id
)的记录,返回包含这些记录的列表,也就是返回某个作者对应的多篇文章记录。
foo = Author(name='Foo') spam = Article(title='Spam') ham = Article(title='Ham')
建立关系有两种方式,第一种方式是为外键字段赋值:
spam.author_id = 1 ham.author_id = 1
调用后,结果如下:
foo.articles # [<Article u'Spam'>, <Article u'Ham'>]
另一种方式是通过操作关系属性,将关系属性赋给实际的对象即可建立关系。
foo.articles.append(spam) foo.articles.append(ham)
我们在 Author
类中定义了集合关系属性 articles
,用来获取某个作者拥有的多篇文章记录。在某些情况下,你也许希望能在 Article
类中定义 一个类似的 author
关系属性,当被调用时返回对应的作者记录,这类返回单个值的关系属性被称为 标量关系属性。而这种两侧都添加关系属性获取对方记录的关系我们称之为 双向关系(bidirectional relationship
)。
双向关系并不是必须的,但在某些情况下会非常方便。双向关系的建立很简单,通过在关系的另一侧也创建一个 relationship()
函数,我们就可以在两个表之间建立双向关系。我们使用作家(Writer
)和书 (Book
)的一对多关系来进行演示:
class Writer(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(70), unique=True) books = db.relationship('Book', back_populates='writer') 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')
需要注意的是,我们只需要在关系的一侧操作关系。当为 Book
对象的 writer
属性赋值后,对应 Writer
对象的 books
属性的返回值也会自动包含这个 Book
对象。反之,当某个 Writer
对象被删除时,对应的 Book
对象的 writer
属性被调用时的返回值也会被置为空(即 NULL
,会返回 None
)。
其他关系模式建立双向关系的方式完全相同,在下面介绍不同的关系模式时我们会简单说明。
在介绍关系函数的参数时,我们曾提到过,使用关系函数中的 backref
参数可以简化双向关系的定义。以一对多关系为例,backref
参数用来自动为关系另一侧添加关系属性,作为反向引用(back reference
),赋予的值会作为关系另一侧的关系属性名称。
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') 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'))
尽管使用 backref
非常方便,但通常来说 “显式好过隐式”,所以我们应该尽量使用 back_populates
定义双向关系。
一对多关系反过来就是多对一关系,这两种关系模式分别从不同的视角出发。
我们在前面介绍过,关系属性在关系模式的出发侧定义。当出发点在 “多” 这一侧时,我们希望在 Citizen
类中添加一个关系属性 city
来获取对应的城市对象,因为这个关系属性返回单个值,我们称之为标量关系属性。在定义关系时,外键总是在 “多” 这一侧定义,所以在多对一关系中外键和关系属性都定义在 “多” 这一侧,即 City
类中。
class Citizen(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(70), unique=True) city_id = db.Column(db.Integer, db.ForeignKey('city.id')) city = db.relationship('City') class City(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(30), unique=True)
这时定义的 city
关系属性是一个标量属性(返回单一数据)。当 Citizen.city
被调用时,SQLAlchemy
会根据外键字段 city_id
存储的值查找对应的 City
对象并返回,即居民记录对应的城市记录。
当建立双向关系时,如果不使用 backref
,那么一对多和多对一关系 模式在定义上完全相同,这时可以将一对多和多对一视为同一种关系模式。在后面我们通常都会为一对多或多对一建立双向关系,这时将弱化这两种关系的区别,一律称为一对多关系。
一对一关系实际上是通过建立双向关系的一对多关系的基础上转化而来。我们要确保关系两侧的关系属性都是标量属性,都只返回单个值,所以要在定义集合属性的关系函数中将 uselist
参数设为 False
,这时一对多关系将被转换为一对一关系。
class Country(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(30), unique=True) capital = db.relationship('Capital', uselist=False) class Capital(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(30), unique=True) country_id = db.Column(db.Integer, db.ForeignKey('country.id')) country = db.relationship('Country')
我们将使用学生和老师来演示多对多关系:每个学生有多个老师, 而每个老师有多个学生。
在一对多关系中,我们可以在 “多” 这一侧添加外键指向 “一” 这一 侧,外键只能存储一个记录,但是在多对多关系中,每一个记录都可以与关系另一侧的多个记录建立关系,关系两侧的模型都需要存储一组外键。
在 SQLAlchemy
中,要想表示多对多关系,除了关系两侧的模型外,我们还需要创建一个关联表(association table
)。关联表不存储数据,只用来存储关系两侧模型的外键对应关系。
association_table = db.Table( 'association', db.Column('student_id', db.Integer, db.ForeignKey('student.id')), db.Column('teacher_id', db.Integer, db.ForeignKey('teacher.id')) ) class Student(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(70), unique=True) grade = db.Column(db.String(20)) teachers = db.relationship('Teacher', secondary=association_table, back_populates='students') class Teacher(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(70), unique=True) office = db.Column(db.String(20))
当我们需要查询某个学生记录的多个老师时,我们先通过学生和关联表的一对多关系查找所有包含该学生的关联表记录,然后就可以从这 些记录中再进一步获取每个关联表记录包含的老师记录。
我们在 Student
类中定义一个 teachers
关系属性用来获取老师集合。 在多对多关系中定义关系函数,除了第一个参数是关系另一侧的模型名称外,我们还需要添加一个 secondary
参数,把这个值设为关联表的名称。
为了便于实现真正的多对多关系,我们需要建立双向关系。建立双向关系后,多对多关系会变得更加直观。在 Student
类上的 teachers
集合 属性会返回所有关联的老师记录,而在 Teacher
类上的 students
集合属性 会返回所有相关的学生记录。
class Student(db.Model): ... teachers = db.relationship('Teacher', secondary=association_table, back_populates='students') class Teacher(db.Model): ... students = db.relationship('Student', secondary=association_table, back_populates='teachers')
关联表由 SQLAlchemy 接管,它会帮我们管理这个表:我们只需要像往常一样通过操作关系属性来建立或解除关系,SQLAlchemy 会自动在关联表中创建或删除对应的关联表记录,而不用手动操作关联表。