, ] ``` 另一种方式是通过操作关系属性,将关系属性赋给实际的对象即可建立关系。集合关系属性可以像列表一样操作,调用append()方法来与一个Article对象建立关系: ``` >>> foo.articles.append(spam) >>> foo.articles.append(ham) >>> db.session.commit() ``` 我们也可以直接将关系属性赋值给一个包含Article对象的列表。 和前面的第一种方式类似,为了让改动生效,我们需要调用db.session.commit()方法提交数据库会话。建立关系后,存储外键的author_id字段会自动获得正确的值,而调用Author实例的关系属性articles时,会获得所有建立关系的Article对象: ``` >>> spam.author_id 1 >>> foo.articles [, ] ``` 和主键类似,外键字段由SQLAlchemy管理,我们不需要手动设置。当通过关系属性建立关系后,外键字段会自动获得正确的值。 >提示 在后面的示例程序中,我们会统一使用第二种方式,即通过关系属性来建立关系。 和append()相对,对关系属性调用remove()方法可以与对应的Aritcle对象解除关系: ``` >>> foo.articles.remove(spam) >>> db.session.commit() >>> foo.articles [] ``` > 提示 你也可以使用pop()方法操作关系属性,它会与关系属性对应的列表的最后一个Aritcle对象解除关系并返回该对象。 不要忘记在操作结束后需要调用commit()方法提交数据库会话,这样才可以把改动写入数据库。 在上面我们提到过,使用关系函数定义的属性不是数据库字段,而是类似于特定的查询函数。当某个Aritcle对象被删除时,在对应Author对象的aritcles属性调用时返回的列表也不会包含该对象。 在关系函数中,有很多参数可以用来设置调用关系属性进行查询时的具体行为。常用的关系函数参数如表5-8所示。  表5-8 常用的SQLAlchemy关系函数参数 当关系属性被调用时,关系函数会加载相应的记录,表5-9列出了控制关系记录加载方式的lazy参数的常用选项。  表5-9 常用的SQLAlchemy关系记录加载方式(lazy参数可选值) > 注意 dynamic选项仅用于集合关系属性,不可用于多对一、一对一或是在关系函数中将uselist参数设为False的情况。 许多教程和示例使用dynamic来动态加载所有集合关系属性对应的记录,这是应该避免的行为。使用dynamic加载方式意味着每次操作关系都会执行一次SQL查询,这会造成潜在的性能问题。大多数情况下我们只需要使用默认值(select),只有在调用关系属性会返回大量记录,并且总是需要对关系属性返回的结果附加额外的查询时才需要使用动态加载(lazy='dynamic')。 ### 4.建立双向关系 我们在Author类中定义了集合关系属性articles,用来获取某个作者拥有的多篇文章记录。在某些情况下,你也许希望能在Article类中定义一个类似的author关系属性,当被调用时返回对应的作者记录,这类返回单个值的关系属性被称为标量关系属性。而这种两侧都添加关系属性获取对方记录的关系我们称之为双向关系(bidirectional relationship)。 双向关系并不是必须的,但在某些情况下会非常方便。双向关系的建立很简单,通过在关系的另一侧也创建一个relationship()函数,我们就可以在两个表之间建立双向关系。我们使用作家(Writer)和书(Book)的一对多关系来进行演示,建立双向关系后的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(作者)记录;而在Writer(作者)类中的books属性则用来获取对应的多个Book(书)记录。在关系函数中,我们使用back_populates参数来连接对方,back_populates参数的值需要设为关系另一侧的关系属性名。 为了方便演示,我们先创建1个Writer和2个Book记录,并添加到数据库中: ``` >>> king = Writer(name='Stephen King') >>> carrie = Book(name='Carrie') >>> it = Book(name='IT') >>> db.session.add(king) >>> db.session.add(carrie) >>> db.session.add(it) >>> db.session.commit() ``` 设置双向关系后,除了通过集合属性books来操作关系,我们也可以使用标量属性writer来进行关系操作。比如,将一个Writer对象赋值给某个Book对象的writer属性,就会和这个Book对象建立关系: ``` >>> carrie.writer = king >>> carrie.writer >>> king.books [] >>> it.writer = writer >>> king.books [, ] ``` 相对的,将某个Book的writer属性设为None,就会解除与对应Writer对象的关系: ``` >>> carrie.writer = None >>> king.books [] >>> 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') 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')) ``` 在定义集合属性songs的关系函数中,我们将backref参数设为singer,这会同时在Song类中添加了一个singer标量属性。这时我们仅需要定义一个关系函数,虽然singer是一个“看不见的关系属性”,但在使用上和定义两个关系函数并使用back_populates参数的效果完全相同。 需要注意的是,使用backref允许我们仅在关系一侧定义另一侧的关系属性,但是在某些情况下,我们希望可以对在关系另一侧的关系属性进行设置,这时就需要使用backref()函数。backref()函数接收第一个参数作为在关系另一侧添加的关系属性名,其他关键字参数会作为关系另一侧关系函数的参数传入。比如,我们要在关系另一侧“看不见的relationship()函数”中将uselist参数设为False,可以这样实现: ``` class Singer(db.Model): ... songs = relationship('Song', backref=backref('singer', uselist=False)) ``` 尽管使用backref非常方便,但通常来说“显式好过隐式”,所以我们应该尽量使用back_populates定义双向关系。为了便于理解,在本书的示例程序中都将使用back_populates来建立双向关系。 # 3、多对一 一对多关系反过来就是多对一关系,这两种关系模式分别从不同的视角出发。一个作者拥有多篇文章,反过来就是多篇文章属于同一个作者。为了便于区分,我们使用居民和城市来演示多对一关系:多个居民居住在同一个城市。多对一关系如图5-5所示。  图5-5 多对一示意图 在示例程序中,Citizen类表示居民,City类表示城市。建立多对一关系后,我们将在Citizen类中创建一个标量关系属性city,调用它可以获取单个City对象。 我们在前面介绍过,关系属性在关系模式的出发侧定义。当出发点在“多”这一侧时,我们希望在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,那么一对多和多对一关系模式在定义上完全相同,这时可以将一对多和多对一视为同一种关系模式。在后面我们通常都会为一对多或多对一建立双向关系,这时将弱化这两种关系的区别,一律称为一对多关系。 # 4、一对一 我们将使用国家和首都来演示一对一关系:每个国家只有一个首都;反过来说,一个城市也只能作为一个国家的首都。一对一关系示意如图5-6所示。  图5-6 一对一关系示意图 在示例程序中,Country类表示国家,Capital类表示首都。建立一对一关系后,我们将在Country类中创建一个标量关系属性capital,调用它会获取单个Capital对象;我们还将在Capital类中创建一个标量关系属性country,调用它会获取单个的Country对象。 一对一关系实际上是通过建立双向关系的一对多关系的基础上转化而来。我们要确保关系两侧的关系属性都是标量属性,都只返回单个值,所以要在定义集合属性的关系函数中将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') ``` “多”这一侧本身就是标量关系属性,不用做任何改动。而“一”这一侧的集合关系属性,通过将uselist设为False后,将仅返回对应的单个记录,而且无法再使用列表语义操作: ``` >>> china = Country(name='China') >>> beijing = Capital(name='Beijing') >>> db.session.add(china) >>> db.session.add(beijing) >>> db.session.commit() >>> china.capital = beijing >>> china.capital >>> beijing.country u'China' >>> tokyo = Capital(name'Tokyo') >>> china.capital.append(tokyo) Traceback (most recent call last): File "", line 1, in AttributeError: 'Capital' object has no attribute 'append' ``` # 5、多对多 我们将使用学生和老师来演示多对多关系:每个学生有多个老师,而每个老师有多个学生。多对多关系模式示意图如图5-7所示。  图5-7 多对多关系示意图 在示例程序中,Student类表示学生,Teacher类表示老师。在这两个模型之间建立多对多关系后,我们需要在Student类中添加一个集合关系属性teachers,调用它可以获取某个学生的多个老师,而不同的学生可以和同一个老师建立关系。 在一对多关系中,我们可以在“多”这一侧添加外键指向“一”这一侧,外键只能存储一个记录,但是在多对多关系中,每一个记录都可以与关系另一侧的多个记录建立关系,关系两侧的模型都需要存储一组外键。在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)) ``` 关联表使用db.Table类定义,传入的第一个参数是关联表的名称。我们在关联表中定义了两个外键字段:teacher_id字段存储Teacher类的主键,student_id存储Student类的主键。借助关联表这个中间人存储的外键对,我们可以把多对多关系分化成两个一对多关系,如图5-8所示。  图5-8 关联表示意图 当我们需要查询某个学生记录的多个老师时,我们先通过学生和关联表的一对多关系查找所有包含该学生的关联表记录,然后就可以从这些记录中再进一步获取每个关联表记录包含的老师记录。以图5-8中的随机数据为例,假设学生记录的id为1,那么通过查找关联表中student_id字段为1的记录,就可以获取到对应的teacher_id值(分别为3和4),通过外键值就可以在teacher表里获取id为3和4的记录,最终,我们就获取到id为1的学生记录相关联的所有老师记录。 我们在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') ``` 除了在声明关系时有所不同,多对多关系模式在操作关系时和其他关系模式基本相同。调用关系属性student.teachers时,SQLAlchemy会直接返回关系另一侧的Teacher对象,而不是关联表记录,反之亦同。和其他关系模式中的集合关系属性一样,我们可以将关系属性teachers和students像列表一样操作。比如,当你需要为某一个学生添加老师时,对关系属性使用append()方法即可。如果你想要解除关系,那么可以使用remove()方法。 > 注意 关联表由SQLAlchemy接管,它会帮我们管理这个表:我们只需要像往常一样通过操作关系属性来建立或解除关系,SQLAlchemy会自动在关联表中创建或删除对应的关联表记录,而不用手动操作关联表。 同样的,在多对多关系中我们也只需要在关系的一侧操作关系。当为学生A的teachers添加了老师B后,调用老师B的students属性时返回的学生记录也会包含学生A,反之亦同。
] ``` 另一种方式是通过操作关系属性,将关系属性赋给实际的对象即可建立关系。集合关系属性可以像列表一样操作,调用append()方法来与一个Article对象建立关系: ``` >>> foo.articles.append(spam) >>> foo.articles.append(ham) >>> db.session.commit() ``` 我们也可以直接将关系属性赋值给一个包含Article对象的列表。 和前面的第一种方式类似,为了让改动生效,我们需要调用db.session.commit()方法提交数据库会话。建立关系后,存储外键的author_id字段会自动获得正确的值,而调用Author实例的关系属性articles时,会获得所有建立关系的Article对象: ``` >>> spam.author_id 1 >>> foo.articles [, ] ``` 和主键类似,外键字段由SQLAlchemy管理,我们不需要手动设置。当通过关系属性建立关系后,外键字段会自动获得正确的值。 >提示 在后面的示例程序中,我们会统一使用第二种方式,即通过关系属性来建立关系。 和append()相对,对关系属性调用remove()方法可以与对应的Aritcle对象解除关系: ``` >>> foo.articles.remove(spam) >>> db.session.commit() >>> foo.articles [] ``` > 提示 你也可以使用pop()方法操作关系属性,它会与关系属性对应的列表的最后一个Aritcle对象解除关系并返回该对象。 不要忘记在操作结束后需要调用commit()方法提交数据库会话,这样才可以把改动写入数据库。 在上面我们提到过,使用关系函数定义的属性不是数据库字段,而是类似于特定的查询函数。当某个Aritcle对象被删除时,在对应Author对象的aritcles属性调用时返回的列表也不会包含该对象。 在关系函数中,有很多参数可以用来设置调用关系属性进行查询时的具体行为。常用的关系函数参数如表5-8所示。  表5-8 常用的SQLAlchemy关系函数参数 当关系属性被调用时,关系函数会加载相应的记录,表5-9列出了控制关系记录加载方式的lazy参数的常用选项。  表5-9 常用的SQLAlchemy关系记录加载方式(lazy参数可选值) > 注意 dynamic选项仅用于集合关系属性,不可用于多对一、一对一或是在关系函数中将uselist参数设为False的情况。 许多教程和示例使用dynamic来动态加载所有集合关系属性对应的记录,这是应该避免的行为。使用dynamic加载方式意味着每次操作关系都会执行一次SQL查询,这会造成潜在的性能问题。大多数情况下我们只需要使用默认值(select),只有在调用关系属性会返回大量记录,并且总是需要对关系属性返回的结果附加额外的查询时才需要使用动态加载(lazy='dynamic')。 ### 4.建立双向关系 我们在Author类中定义了集合关系属性articles,用来获取某个作者拥有的多篇文章记录。在某些情况下,你也许希望能在Article类中定义一个类似的author关系属性,当被调用时返回对应的作者记录,这类返回单个值的关系属性被称为标量关系属性。而这种两侧都添加关系属性获取对方记录的关系我们称之为双向关系(bidirectional relationship)。 双向关系并不是必须的,但在某些情况下会非常方便。双向关系的建立很简单,通过在关系的另一侧也创建一个relationship()函数,我们就可以在两个表之间建立双向关系。我们使用作家(Writer)和书(Book)的一对多关系来进行演示,建立双向关系后的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(作者)记录;而在Writer(作者)类中的books属性则用来获取对应的多个Book(书)记录。在关系函数中,我们使用back_populates参数来连接对方,back_populates参数的值需要设为关系另一侧的关系属性名。 为了方便演示,我们先创建1个Writer和2个Book记录,并添加到数据库中: ``` >>> king = Writer(name='Stephen King') >>> carrie = Book(name='Carrie') >>> it = Book(name='IT') >>> db.session.add(king) >>> db.session.add(carrie) >>> db.session.add(it) >>> db.session.commit() ``` 设置双向关系后,除了通过集合属性books来操作关系,我们也可以使用标量属性writer来进行关系操作。比如,将一个Writer对象赋值给某个Book对象的writer属性,就会和这个Book对象建立关系: ``` >>> carrie.writer = king >>> carrie.writer >>> king.books [] >>> it.writer = writer >>> king.books [, ] ``` 相对的,将某个Book的writer属性设为None,就会解除与对应Writer对象的关系: ``` >>> carrie.writer = None >>> king.books [] >>> 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') 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')) ``` 在定义集合属性songs的关系函数中,我们将backref参数设为singer,这会同时在Song类中添加了一个singer标量属性。这时我们仅需要定义一个关系函数,虽然singer是一个“看不见的关系属性”,但在使用上和定义两个关系函数并使用back_populates参数的效果完全相同。 需要注意的是,使用backref允许我们仅在关系一侧定义另一侧的关系属性,但是在某些情况下,我们希望可以对在关系另一侧的关系属性进行设置,这时就需要使用backref()函数。backref()函数接收第一个参数作为在关系另一侧添加的关系属性名,其他关键字参数会作为关系另一侧关系函数的参数传入。比如,我们要在关系另一侧“看不见的relationship()函数”中将uselist参数设为False,可以这样实现: ``` class Singer(db.Model): ... songs = relationship('Song', backref=backref('singer', uselist=False)) ``` 尽管使用backref非常方便,但通常来说“显式好过隐式”,所以我们应该尽量使用back_populates定义双向关系。为了便于理解,在本书的示例程序中都将使用back_populates来建立双向关系。 # 3、多对一 一对多关系反过来就是多对一关系,这两种关系模式分别从不同的视角出发。一个作者拥有多篇文章,反过来就是多篇文章属于同一个作者。为了便于区分,我们使用居民和城市来演示多对一关系:多个居民居住在同一个城市。多对一关系如图5-5所示。  图5-5 多对一示意图 在示例程序中,Citizen类表示居民,City类表示城市。建立多对一关系后,我们将在Citizen类中创建一个标量关系属性city,调用它可以获取单个City对象。 我们在前面介绍过,关系属性在关系模式的出发侧定义。当出发点在“多”这一侧时,我们希望在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,那么一对多和多对一关系模式在定义上完全相同,这时可以将一对多和多对一视为同一种关系模式。在后面我们通常都会为一对多或多对一建立双向关系,这时将弱化这两种关系的区别,一律称为一对多关系。 # 4、一对一 我们将使用国家和首都来演示一对一关系:每个国家只有一个首都;反过来说,一个城市也只能作为一个国家的首都。一对一关系示意如图5-6所示。  图5-6 一对一关系示意图 在示例程序中,Country类表示国家,Capital类表示首都。建立一对一关系后,我们将在Country类中创建一个标量关系属性capital,调用它会获取单个Capital对象;我们还将在Capital类中创建一个标量关系属性country,调用它会获取单个的Country对象。 一对一关系实际上是通过建立双向关系的一对多关系的基础上转化而来。我们要确保关系两侧的关系属性都是标量属性,都只返回单个值,所以要在定义集合属性的关系函数中将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') ``` “多”这一侧本身就是标量关系属性,不用做任何改动。而“一”这一侧的集合关系属性,通过将uselist设为False后,将仅返回对应的单个记录,而且无法再使用列表语义操作: ``` >>> china = Country(name='China') >>> beijing = Capital(name='Beijing') >>> db.session.add(china) >>> db.session.add(beijing) >>> db.session.commit() >>> china.capital = beijing >>> china.capital >>> beijing.country u'China' >>> tokyo = Capital(name'Tokyo') >>> china.capital.append(tokyo) Traceback (most recent call last): File "", line 1, in AttributeError: 'Capital' object has no attribute 'append' ``` # 5、多对多 我们将使用学生和老师来演示多对多关系:每个学生有多个老师,而每个老师有多个学生。多对多关系模式示意图如图5-7所示。  图5-7 多对多关系示意图 在示例程序中,Student类表示学生,Teacher类表示老师。在这两个模型之间建立多对多关系后,我们需要在Student类中添加一个集合关系属性teachers,调用它可以获取某个学生的多个老师,而不同的学生可以和同一个老师建立关系。 在一对多关系中,我们可以在“多”这一侧添加外键指向“一”这一侧,外键只能存储一个记录,但是在多对多关系中,每一个记录都可以与关系另一侧的多个记录建立关系,关系两侧的模型都需要存储一组外键。在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)) ``` 关联表使用db.Table类定义,传入的第一个参数是关联表的名称。我们在关联表中定义了两个外键字段:teacher_id字段存储Teacher类的主键,student_id存储Student类的主键。借助关联表这个中间人存储的外键对,我们可以把多对多关系分化成两个一对多关系,如图5-8所示。  图5-8 关联表示意图 当我们需要查询某个学生记录的多个老师时,我们先通过学生和关联表的一对多关系查找所有包含该学生的关联表记录,然后就可以从这些记录中再进一步获取每个关联表记录包含的老师记录。以图5-8中的随机数据为例,假设学生记录的id为1,那么通过查找关联表中student_id字段为1的记录,就可以获取到对应的teacher_id值(分别为3和4),通过外键值就可以在teacher表里获取id为3和4的记录,最终,我们就获取到id为1的学生记录相关联的所有老师记录。 我们在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') ``` 除了在声明关系时有所不同,多对多关系模式在操作关系时和其他关系模式基本相同。调用关系属性student.teachers时,SQLAlchemy会直接返回关系另一侧的Teacher对象,而不是关联表记录,反之亦同。和其他关系模式中的集合关系属性一样,我们可以将关系属性teachers和students像列表一样操作。比如,当你需要为某一个学生添加老师时,对关系属性使用append()方法即可。如果你想要解除关系,那么可以使用remove()方法。 > 注意 关联表由SQLAlchemy接管,它会帮我们管理这个表:我们只需要像往常一样通过操作关系属性来建立或解除关系,SQLAlchemy会自动在关联表中创建或删除对应的关联表记录,而不用手动操作关联表。 同样的,在多对多关系中我们也只需要在关系的一侧操作关系。当为学生A的teachers添加了老师B后,调用老师B的students属性时返回的学生记录也会包含学生A,反之亦同。
, ] ``` 和主键类似,外键字段由SQLAlchemy管理,我们不需要手动设置。当通过关系属性建立关系后,外键字段会自动获得正确的值。 >提示 在后面的示例程序中,我们会统一使用第二种方式,即通过关系属性来建立关系。 和append()相对,对关系属性调用remove()方法可以与对应的Aritcle对象解除关系: ``` >>> foo.articles.remove(spam) >>> db.session.commit() >>> foo.articles [] ``` > 提示 你也可以使用pop()方法操作关系属性,它会与关系属性对应的列表的最后一个Aritcle对象解除关系并返回该对象。 不要忘记在操作结束后需要调用commit()方法提交数据库会话,这样才可以把改动写入数据库。 在上面我们提到过,使用关系函数定义的属性不是数据库字段,而是类似于特定的查询函数。当某个Aritcle对象被删除时,在对应Author对象的aritcles属性调用时返回的列表也不会包含该对象。 在关系函数中,有很多参数可以用来设置调用关系属性进行查询时的具体行为。常用的关系函数参数如表5-8所示。  表5-8 常用的SQLAlchemy关系函数参数 当关系属性被调用时,关系函数会加载相应的记录,表5-9列出了控制关系记录加载方式的lazy参数的常用选项。  表5-9 常用的SQLAlchemy关系记录加载方式(lazy参数可选值) > 注意 dynamic选项仅用于集合关系属性,不可用于多对一、一对一或是在关系函数中将uselist参数设为False的情况。 许多教程和示例使用dynamic来动态加载所有集合关系属性对应的记录,这是应该避免的行为。使用dynamic加载方式意味着每次操作关系都会执行一次SQL查询,这会造成潜在的性能问题。大多数情况下我们只需要使用默认值(select),只有在调用关系属性会返回大量记录,并且总是需要对关系属性返回的结果附加额外的查询时才需要使用动态加载(lazy='dynamic')。 ### 4.建立双向关系 我们在Author类中定义了集合关系属性articles,用来获取某个作者拥有的多篇文章记录。在某些情况下,你也许希望能在Article类中定义一个类似的author关系属性,当被调用时返回对应的作者记录,这类返回单个值的关系属性被称为标量关系属性。而这种两侧都添加关系属性获取对方记录的关系我们称之为双向关系(bidirectional relationship)。 双向关系并不是必须的,但在某些情况下会非常方便。双向关系的建立很简单,通过在关系的另一侧也创建一个relationship()函数,我们就可以在两个表之间建立双向关系。我们使用作家(Writer)和书(Book)的一对多关系来进行演示,建立双向关系后的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(作者)记录;而在Writer(作者)类中的books属性则用来获取对应的多个Book(书)记录。在关系函数中,我们使用back_populates参数来连接对方,back_populates参数的值需要设为关系另一侧的关系属性名。 为了方便演示,我们先创建1个Writer和2个Book记录,并添加到数据库中: ``` >>> king = Writer(name='Stephen King') >>> carrie = Book(name='Carrie') >>> it = Book(name='IT') >>> db.session.add(king) >>> db.session.add(carrie) >>> db.session.add(it) >>> db.session.commit() ``` 设置双向关系后,除了通过集合属性books来操作关系,我们也可以使用标量属性writer来进行关系操作。比如,将一个Writer对象赋值给某个Book对象的writer属性,就会和这个Book对象建立关系: ``` >>> carrie.writer = king >>> carrie.writer >>> king.books [] >>> it.writer = writer >>> king.books [, ] ``` 相对的,将某个Book的writer属性设为None,就会解除与对应Writer对象的关系: ``` >>> carrie.writer = None >>> king.books [] >>> 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') 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')) ``` 在定义集合属性songs的关系函数中,我们将backref参数设为singer,这会同时在Song类中添加了一个singer标量属性。这时我们仅需要定义一个关系函数,虽然singer是一个“看不见的关系属性”,但在使用上和定义两个关系函数并使用back_populates参数的效果完全相同。 需要注意的是,使用backref允许我们仅在关系一侧定义另一侧的关系属性,但是在某些情况下,我们希望可以对在关系另一侧的关系属性进行设置,这时就需要使用backref()函数。backref()函数接收第一个参数作为在关系另一侧添加的关系属性名,其他关键字参数会作为关系另一侧关系函数的参数传入。比如,我们要在关系另一侧“看不见的relationship()函数”中将uselist参数设为False,可以这样实现: ``` class Singer(db.Model): ... songs = relationship('Song', backref=backref('singer', uselist=False)) ``` 尽管使用backref非常方便,但通常来说“显式好过隐式”,所以我们应该尽量使用back_populates定义双向关系。为了便于理解,在本书的示例程序中都将使用back_populates来建立双向关系。 # 3、多对一 一对多关系反过来就是多对一关系,这两种关系模式分别从不同的视角出发。一个作者拥有多篇文章,反过来就是多篇文章属于同一个作者。为了便于区分,我们使用居民和城市来演示多对一关系:多个居民居住在同一个城市。多对一关系如图5-5所示。  图5-5 多对一示意图 在示例程序中,Citizen类表示居民,City类表示城市。建立多对一关系后,我们将在Citizen类中创建一个标量关系属性city,调用它可以获取单个City对象。 我们在前面介绍过,关系属性在关系模式的出发侧定义。当出发点在“多”这一侧时,我们希望在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,那么一对多和多对一关系模式在定义上完全相同,这时可以将一对多和多对一视为同一种关系模式。在后面我们通常都会为一对多或多对一建立双向关系,这时将弱化这两种关系的区别,一律称为一对多关系。 # 4、一对一 我们将使用国家和首都来演示一对一关系:每个国家只有一个首都;反过来说,一个城市也只能作为一个国家的首都。一对一关系示意如图5-6所示。  图5-6 一对一关系示意图 在示例程序中,Country类表示国家,Capital类表示首都。建立一对一关系后,我们将在Country类中创建一个标量关系属性capital,调用它会获取单个Capital对象;我们还将在Capital类中创建一个标量关系属性country,调用它会获取单个的Country对象。 一对一关系实际上是通过建立双向关系的一对多关系的基础上转化而来。我们要确保关系两侧的关系属性都是标量属性,都只返回单个值,所以要在定义集合属性的关系函数中将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') ``` “多”这一侧本身就是标量关系属性,不用做任何改动。而“一”这一侧的集合关系属性,通过将uselist设为False后,将仅返回对应的单个记录,而且无法再使用列表语义操作: ``` >>> china = Country(name='China') >>> beijing = Capital(name='Beijing') >>> db.session.add(china) >>> db.session.add(beijing) >>> db.session.commit() >>> china.capital = beijing >>> china.capital >>> beijing.country u'China' >>> tokyo = Capital(name'Tokyo') >>> china.capital.append(tokyo) Traceback (most recent call last): File "", line 1, in AttributeError: 'Capital' object has no attribute 'append' ``` # 5、多对多 我们将使用学生和老师来演示多对多关系:每个学生有多个老师,而每个老师有多个学生。多对多关系模式示意图如图5-7所示。  图5-7 多对多关系示意图 在示例程序中,Student类表示学生,Teacher类表示老师。在这两个模型之间建立多对多关系后,我们需要在Student类中添加一个集合关系属性teachers,调用它可以获取某个学生的多个老师,而不同的学生可以和同一个老师建立关系。 在一对多关系中,我们可以在“多”这一侧添加外键指向“一”这一侧,外键只能存储一个记录,但是在多对多关系中,每一个记录都可以与关系另一侧的多个记录建立关系,关系两侧的模型都需要存储一组外键。在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)) ``` 关联表使用db.Table类定义,传入的第一个参数是关联表的名称。我们在关联表中定义了两个外键字段:teacher_id字段存储Teacher类的主键,student_id存储Student类的主键。借助关联表这个中间人存储的外键对,我们可以把多对多关系分化成两个一对多关系,如图5-8所示。  图5-8 关联表示意图 当我们需要查询某个学生记录的多个老师时,我们先通过学生和关联表的一对多关系查找所有包含该学生的关联表记录,然后就可以从这些记录中再进一步获取每个关联表记录包含的老师记录。以图5-8中的随机数据为例,假设学生记录的id为1,那么通过查找关联表中student_id字段为1的记录,就可以获取到对应的teacher_id值(分别为3和4),通过外键值就可以在teacher表里获取id为3和4的记录,最终,我们就获取到id为1的学生记录相关联的所有老师记录。 我们在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') ``` 除了在声明关系时有所不同,多对多关系模式在操作关系时和其他关系模式基本相同。调用关系属性student.teachers时,SQLAlchemy会直接返回关系另一侧的Teacher对象,而不是关联表记录,反之亦同。和其他关系模式中的集合关系属性一样,我们可以将关系属性teachers和students像列表一样操作。比如,当你需要为某一个学生添加老师时,对关系属性使用append()方法即可。如果你想要解除关系,那么可以使用remove()方法。 > 注意 关联表由SQLAlchemy接管,它会帮我们管理这个表:我们只需要像往常一样通过操作关系属性来建立或解除关系,SQLAlchemy会自动在关联表中创建或删除对应的关联表记录,而不用手动操作关联表。 同样的,在多对多关系中我们也只需要在关系的一侧操作关系。当为学生A的teachers添加了老师B后,调用老师B的students属性时返回的学生记录也会包含学生A,反之亦同。
] ``` 和主键类似,外键字段由SQLAlchemy管理,我们不需要手动设置。当通过关系属性建立关系后,外键字段会自动获得正确的值。 >提示 在后面的示例程序中,我们会统一使用第二种方式,即通过关系属性来建立关系。 和append()相对,对关系属性调用remove()方法可以与对应的Aritcle对象解除关系: ``` >>> foo.articles.remove(spam) >>> db.session.commit() >>> foo.articles [] ``` > 提示 你也可以使用pop()方法操作关系属性,它会与关系属性对应的列表的最后一个Aritcle对象解除关系并返回该对象。 不要忘记在操作结束后需要调用commit()方法提交数据库会话,这样才可以把改动写入数据库。 在上面我们提到过,使用关系函数定义的属性不是数据库字段,而是类似于特定的查询函数。当某个Aritcle对象被删除时,在对应Author对象的aritcles属性调用时返回的列表也不会包含该对象。 在关系函数中,有很多参数可以用来设置调用关系属性进行查询时的具体行为。常用的关系函数参数如表5-8所示。  表5-8 常用的SQLAlchemy关系函数参数 当关系属性被调用时,关系函数会加载相应的记录,表5-9列出了控制关系记录加载方式的lazy参数的常用选项。  表5-9 常用的SQLAlchemy关系记录加载方式(lazy参数可选值) > 注意 dynamic选项仅用于集合关系属性,不可用于多对一、一对一或是在关系函数中将uselist参数设为False的情况。 许多教程和示例使用dynamic来动态加载所有集合关系属性对应的记录,这是应该避免的行为。使用dynamic加载方式意味着每次操作关系都会执行一次SQL查询,这会造成潜在的性能问题。大多数情况下我们只需要使用默认值(select),只有在调用关系属性会返回大量记录,并且总是需要对关系属性返回的结果附加额外的查询时才需要使用动态加载(lazy='dynamic')。 ### 4.建立双向关系 我们在Author类中定义了集合关系属性articles,用来获取某个作者拥有的多篇文章记录。在某些情况下,你也许希望能在Article类中定义一个类似的author关系属性,当被调用时返回对应的作者记录,这类返回单个值的关系属性被称为标量关系属性。而这种两侧都添加关系属性获取对方记录的关系我们称之为双向关系(bidirectional relationship)。 双向关系并不是必须的,但在某些情况下会非常方便。双向关系的建立很简单,通过在关系的另一侧也创建一个relationship()函数,我们就可以在两个表之间建立双向关系。我们使用作家(Writer)和书(Book)的一对多关系来进行演示,建立双向关系后的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(作者)记录;而在Writer(作者)类中的books属性则用来获取对应的多个Book(书)记录。在关系函数中,我们使用back_populates参数来连接对方,back_populates参数的值需要设为关系另一侧的关系属性名。 为了方便演示,我们先创建1个Writer和2个Book记录,并添加到数据库中: ``` >>> king = Writer(name='Stephen King') >>> carrie = Book(name='Carrie') >>> it = Book(name='IT') >>> db.session.add(king) >>> db.session.add(carrie) >>> db.session.add(it) >>> db.session.commit() ``` 设置双向关系后,除了通过集合属性books来操作关系,我们也可以使用标量属性writer来进行关系操作。比如,将一个Writer对象赋值给某个Book对象的writer属性,就会和这个Book对象建立关系: ``` >>> carrie.writer = king >>> carrie.writer >>> king.books [] >>> it.writer = writer >>> king.books [, ] ``` 相对的,将某个Book的writer属性设为None,就会解除与对应Writer对象的关系: ``` >>> carrie.writer = None >>> king.books [] >>> 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') 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')) ``` 在定义集合属性songs的关系函数中,我们将backref参数设为singer,这会同时在Song类中添加了一个singer标量属性。这时我们仅需要定义一个关系函数,虽然singer是一个“看不见的关系属性”,但在使用上和定义两个关系函数并使用back_populates参数的效果完全相同。 需要注意的是,使用backref允许我们仅在关系一侧定义另一侧的关系属性,但是在某些情况下,我们希望可以对在关系另一侧的关系属性进行设置,这时就需要使用backref()函数。backref()函数接收第一个参数作为在关系另一侧添加的关系属性名,其他关键字参数会作为关系另一侧关系函数的参数传入。比如,我们要在关系另一侧“看不见的relationship()函数”中将uselist参数设为False,可以这样实现: ``` class Singer(db.Model): ... songs = relationship('Song', backref=backref('singer', uselist=False)) ``` 尽管使用backref非常方便,但通常来说“显式好过隐式”,所以我们应该尽量使用back_populates定义双向关系。为了便于理解,在本书的示例程序中都将使用back_populates来建立双向关系。 # 3、多对一 一对多关系反过来就是多对一关系,这两种关系模式分别从不同的视角出发。一个作者拥有多篇文章,反过来就是多篇文章属于同一个作者。为了便于区分,我们使用居民和城市来演示多对一关系:多个居民居住在同一个城市。多对一关系如图5-5所示。  图5-5 多对一示意图 在示例程序中,Citizen类表示居民,City类表示城市。建立多对一关系后,我们将在Citizen类中创建一个标量关系属性city,调用它可以获取单个City对象。 我们在前面介绍过,关系属性在关系模式的出发侧定义。当出发点在“多”这一侧时,我们希望在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,那么一对多和多对一关系模式在定义上完全相同,这时可以将一对多和多对一视为同一种关系模式。在后面我们通常都会为一对多或多对一建立双向关系,这时将弱化这两种关系的区别,一律称为一对多关系。 # 4、一对一 我们将使用国家和首都来演示一对一关系:每个国家只有一个首都;反过来说,一个城市也只能作为一个国家的首都。一对一关系示意如图5-6所示。  图5-6 一对一关系示意图 在示例程序中,Country类表示国家,Capital类表示首都。建立一对一关系后,我们将在Country类中创建一个标量关系属性capital,调用它会获取单个Capital对象;我们还将在Capital类中创建一个标量关系属性country,调用它会获取单个的Country对象。 一对一关系实际上是通过建立双向关系的一对多关系的基础上转化而来。我们要确保关系两侧的关系属性都是标量属性,都只返回单个值,所以要在定义集合属性的关系函数中将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') ``` “多”这一侧本身就是标量关系属性,不用做任何改动。而“一”这一侧的集合关系属性,通过将uselist设为False后,将仅返回对应的单个记录,而且无法再使用列表语义操作: ``` >>> china = Country(name='China') >>> beijing = Capital(name='Beijing') >>> db.session.add(china) >>> db.session.add(beijing) >>> db.session.commit() >>> china.capital = beijing >>> china.capital >>> beijing.country u'China' >>> tokyo = Capital(name'Tokyo') >>> china.capital.append(tokyo) Traceback (most recent call last): File "", line 1, in AttributeError: 'Capital' object has no attribute 'append' ``` # 5、多对多 我们将使用学生和老师来演示多对多关系:每个学生有多个老师,而每个老师有多个学生。多对多关系模式示意图如图5-7所示。  图5-7 多对多关系示意图 在示例程序中,Student类表示学生,Teacher类表示老师。在这两个模型之间建立多对多关系后,我们需要在Student类中添加一个集合关系属性teachers,调用它可以获取某个学生的多个老师,而不同的学生可以和同一个老师建立关系。 在一对多关系中,我们可以在“多”这一侧添加外键指向“一”这一侧,外键只能存储一个记录,但是在多对多关系中,每一个记录都可以与关系另一侧的多个记录建立关系,关系两侧的模型都需要存储一组外键。在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)) ``` 关联表使用db.Table类定义,传入的第一个参数是关联表的名称。我们在关联表中定义了两个外键字段:teacher_id字段存储Teacher类的主键,student_id存储Student类的主键。借助关联表这个中间人存储的外键对,我们可以把多对多关系分化成两个一对多关系,如图5-8所示。  图5-8 关联表示意图 当我们需要查询某个学生记录的多个老师时,我们先通过学生和关联表的一对多关系查找所有包含该学生的关联表记录,然后就可以从这些记录中再进一步获取每个关联表记录包含的老师记录。以图5-8中的随机数据为例,假设学生记录的id为1,那么通过查找关联表中student_id字段为1的记录,就可以获取到对应的teacher_id值(分别为3和4),通过外键值就可以在teacher表里获取id为3和4的记录,最终,我们就获取到id为1的学生记录相关联的所有老师记录。 我们在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') ``` 除了在声明关系时有所不同,多对多关系模式在操作关系时和其他关系模式基本相同。调用关系属性student.teachers时,SQLAlchemy会直接返回关系另一侧的Teacher对象,而不是关联表记录,反之亦同。和其他关系模式中的集合关系属性一样,我们可以将关系属性teachers和students像列表一样操作。比如,当你需要为某一个学生添加老师时,对关系属性使用append()方法即可。如果你想要解除关系,那么可以使用remove()方法。 > 注意 关联表由SQLAlchemy接管,它会帮我们管理这个表:我们只需要像往常一样通过操作关系属性来建立或解除关系,SQLAlchemy会自动在关联表中创建或删除对应的关联表记录,而不用手动操作关联表。 同样的,在多对多关系中我们也只需要在关系的一侧操作关系。当为学生A的teachers添加了老师B后,调用老师B的students属性时返回的学生记录也会包含学生A,反之亦同。
] ``` > 提示 你也可以使用pop()方法操作关系属性,它会与关系属性对应的列表的最后一个Aritcle对象解除关系并返回该对象。 不要忘记在操作结束后需要调用commit()方法提交数据库会话,这样才可以把改动写入数据库。 在上面我们提到过,使用关系函数定义的属性不是数据库字段,而是类似于特定的查询函数。当某个Aritcle对象被删除时,在对应Author对象的aritcles属性调用时返回的列表也不会包含该对象。 在关系函数中,有很多参数可以用来设置调用关系属性进行查询时的具体行为。常用的关系函数参数如表5-8所示。  表5-8 常用的SQLAlchemy关系函数参数 当关系属性被调用时,关系函数会加载相应的记录,表5-9列出了控制关系记录加载方式的lazy参数的常用选项。  表5-9 常用的SQLAlchemy关系记录加载方式(lazy参数可选值) > 注意 dynamic选项仅用于集合关系属性,不可用于多对一、一对一或是在关系函数中将uselist参数设为False的情况。 许多教程和示例使用dynamic来动态加载所有集合关系属性对应的记录,这是应该避免的行为。使用dynamic加载方式意味着每次操作关系都会执行一次SQL查询,这会造成潜在的性能问题。大多数情况下我们只需要使用默认值(select),只有在调用关系属性会返回大量记录,并且总是需要对关系属性返回的结果附加额外的查询时才需要使用动态加载(lazy='dynamic')。 ### 4.建立双向关系 我们在Author类中定义了集合关系属性articles,用来获取某个作者拥有的多篇文章记录。在某些情况下,你也许希望能在Article类中定义一个类似的author关系属性,当被调用时返回对应的作者记录,这类返回单个值的关系属性被称为标量关系属性。而这种两侧都添加关系属性获取对方记录的关系我们称之为双向关系(bidirectional relationship)。 双向关系并不是必须的,但在某些情况下会非常方便。双向关系的建立很简单,通过在关系的另一侧也创建一个relationship()函数,我们就可以在两个表之间建立双向关系。我们使用作家(Writer)和书(Book)的一对多关系来进行演示,建立双向关系后的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(作者)记录;而在Writer(作者)类中的books属性则用来获取对应的多个Book(书)记录。在关系函数中,我们使用back_populates参数来连接对方,back_populates参数的值需要设为关系另一侧的关系属性名。 为了方便演示,我们先创建1个Writer和2个Book记录,并添加到数据库中: ``` >>> king = Writer(name='Stephen King') >>> carrie = Book(name='Carrie') >>> it = Book(name='IT') >>> db.session.add(king) >>> db.session.add(carrie) >>> db.session.add(it) >>> db.session.commit() ``` 设置双向关系后,除了通过集合属性books来操作关系,我们也可以使用标量属性writer来进行关系操作。比如,将一个Writer对象赋值给某个Book对象的writer属性,就会和这个Book对象建立关系: ``` >>> carrie.writer = king >>> carrie.writer >>> king.books [] >>> it.writer = writer >>> king.books [, ] ``` 相对的,将某个Book的writer属性设为None,就会解除与对应Writer对象的关系: ``` >>> carrie.writer = None >>> king.books [] >>> 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') 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')) ``` 在定义集合属性songs的关系函数中,我们将backref参数设为singer,这会同时在Song类中添加了一个singer标量属性。这时我们仅需要定义一个关系函数,虽然singer是一个“看不见的关系属性”,但在使用上和定义两个关系函数并使用back_populates参数的效果完全相同。 需要注意的是,使用backref允许我们仅在关系一侧定义另一侧的关系属性,但是在某些情况下,我们希望可以对在关系另一侧的关系属性进行设置,这时就需要使用backref()函数。backref()函数接收第一个参数作为在关系另一侧添加的关系属性名,其他关键字参数会作为关系另一侧关系函数的参数传入。比如,我们要在关系另一侧“看不见的relationship()函数”中将uselist参数设为False,可以这样实现: ``` class Singer(db.Model): ... songs = relationship('Song', backref=backref('singer', uselist=False)) ``` 尽管使用backref非常方便,但通常来说“显式好过隐式”,所以我们应该尽量使用back_populates定义双向关系。为了便于理解,在本书的示例程序中都将使用back_populates来建立双向关系。 # 3、多对一 一对多关系反过来就是多对一关系,这两种关系模式分别从不同的视角出发。一个作者拥有多篇文章,反过来就是多篇文章属于同一个作者。为了便于区分,我们使用居民和城市来演示多对一关系:多个居民居住在同一个城市。多对一关系如图5-5所示。  图5-5 多对一示意图 在示例程序中,Citizen类表示居民,City类表示城市。建立多对一关系后,我们将在Citizen类中创建一个标量关系属性city,调用它可以获取单个City对象。 我们在前面介绍过,关系属性在关系模式的出发侧定义。当出发点在“多”这一侧时,我们希望在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,那么一对多和多对一关系模式在定义上完全相同,这时可以将一对多和多对一视为同一种关系模式。在后面我们通常都会为一对多或多对一建立双向关系,这时将弱化这两种关系的区别,一律称为一对多关系。 # 4、一对一 我们将使用国家和首都来演示一对一关系:每个国家只有一个首都;反过来说,一个城市也只能作为一个国家的首都。一对一关系示意如图5-6所示。  图5-6 一对一关系示意图 在示例程序中,Country类表示国家,Capital类表示首都。建立一对一关系后,我们将在Country类中创建一个标量关系属性capital,调用它会获取单个Capital对象;我们还将在Capital类中创建一个标量关系属性country,调用它会获取单个的Country对象。 一对一关系实际上是通过建立双向关系的一对多关系的基础上转化而来。我们要确保关系两侧的关系属性都是标量属性,都只返回单个值,所以要在定义集合属性的关系函数中将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') ``` “多”这一侧本身就是标量关系属性,不用做任何改动。而“一”这一侧的集合关系属性,通过将uselist设为False后,将仅返回对应的单个记录,而且无法再使用列表语义操作: ``` >>> china = Country(name='China') >>> beijing = Capital(name='Beijing') >>> db.session.add(china) >>> db.session.add(beijing) >>> db.session.commit() >>> china.capital = beijing >>> china.capital >>> beijing.country u'China' >>> tokyo = Capital(name'Tokyo') >>> china.capital.append(tokyo) Traceback (most recent call last): File "", line 1, in AttributeError: 'Capital' object has no attribute 'append' ``` # 5、多对多 我们将使用学生和老师来演示多对多关系:每个学生有多个老师,而每个老师有多个学生。多对多关系模式示意图如图5-7所示。  图5-7 多对多关系示意图 在示例程序中,Student类表示学生,Teacher类表示老师。在这两个模型之间建立多对多关系后,我们需要在Student类中添加一个集合关系属性teachers,调用它可以获取某个学生的多个老师,而不同的学生可以和同一个老师建立关系。 在一对多关系中,我们可以在“多”这一侧添加外键指向“一”这一侧,外键只能存储一个记录,但是在多对多关系中,每一个记录都可以与关系另一侧的多个记录建立关系,关系两侧的模型都需要存储一组外键。在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)) ``` 关联表使用db.Table类定义,传入的第一个参数是关联表的名称。我们在关联表中定义了两个外键字段:teacher_id字段存储Teacher类的主键,student_id存储Student类的主键。借助关联表这个中间人存储的外键对,我们可以把多对多关系分化成两个一对多关系,如图5-8所示。  图5-8 关联表示意图 当我们需要查询某个学生记录的多个老师时,我们先通过学生和关联表的一对多关系查找所有包含该学生的关联表记录,然后就可以从这些记录中再进一步获取每个关联表记录包含的老师记录。以图5-8中的随机数据为例,假设学生记录的id为1,那么通过查找关联表中student_id字段为1的记录,就可以获取到对应的teacher_id值(分别为3和4),通过外键值就可以在teacher表里获取id为3和4的记录,最终,我们就获取到id为1的学生记录相关联的所有老师记录。 我们在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') ``` 除了在声明关系时有所不同,多对多关系模式在操作关系时和其他关系模式基本相同。调用关系属性student.teachers时,SQLAlchemy会直接返回关系另一侧的Teacher对象,而不是关联表记录,反之亦同。和其他关系模式中的集合关系属性一样,我们可以将关系属性teachers和students像列表一样操作。比如,当你需要为某一个学生添加老师时,对关系属性使用append()方法即可。如果你想要解除关系,那么可以使用remove()方法。 > 注意 关联表由SQLAlchemy接管,它会帮我们管理这个表:我们只需要像往常一样通过操作关系属性来建立或解除关系,SQLAlchemy会自动在关联表中创建或删除对应的关联表记录,而不用手动操作关联表。 同样的,在多对多关系中我们也只需要在关系的一侧操作关系。当为学生A的teachers添加了老师B后,调用老师B的students属性时返回的学生记录也会包含学生A,反之亦同。