python-复盘-flask-数据库一对一/一对多/多对多关系

Flask 数据库多对多关系

实现学生与学院之间的多对多关系



这个例子中的关联表是 registrations,表中的每一行都表示一个学生注册的一个课程。查询多对多关系要分成两步。 若想知道某位学生选择了哪些课程,你要先从学生和注册之间的一对多关系开始, 获取这位学生在 registrations 表中的所有记录,然后再按照多到一的方向遍历课程和注册之间的一对多关系, 找到这位学生在 registrations 表中各记录所对应的课程。 同样,若想找到选择了某门课程的所有学生,你要先从课程表中开始,获取其在 registrations 表中的记录,再获取这些记录联接的学生。通过遍历两个关系来获取查询结果的做法听起来有难度, 不过像前例这种简单关系,SQLAlchemy 就可以完成大部分操作。

registrations = db.Table('registrations',  
    db.Column('student_id', db.Integer, db.ForeignKey('students.id')),  
    db.Column('class_id', db.Integer, db.ForeignKey('classes.id'))  
)  
   
class Student(db.Model):  
    id = db.Column(db.Integer, primary_key=True)  
   name = db.Column(db.String)  
   classes = db.relationship('Class',secondary=registrations,  
                                    backref=db.backref('students', lazy='dynamic'),  
                                    lazy='dynamic')  
  
class Class(db.Model):  
    id = db.Column(db.Integer, primary_key = True)  
    name = db.Column(db.String) 

多对多关系仍使用定义一对多关系的 db.relationship() 方法进行定义,但在多对多关系中,必须把 secondary 参数设为关联表。多对多关系可以在任何一个类中定义, backref 参数会处理好关系的另一侧。关联表就是一个简单的表,不是模型,SQLAlchemy 会自动接管这个表。
这样处理多对多关系特别简单。假设学生是 s,课程是 c,学生注册课程的代码为:

>>> s.classes.append(c)
>>> db.session.add(s)

列出学生 s 注册的课程以及注册了课程 c 的学生也很简单:

>>> s.classes.all()
>>> c.students.all()

Class 模型中的 students 关系由参数 db.backref() 定义。注意,这个关系中还指定了 lazy= 'dynamic' 参数,所以关系两侧返回的查询都可接受额外的过滤器。
如果后来学生 s 决定不选课程 c 了,那么可使用下面的代码更新数据库:

>>> s.classes.remove(c)

下面来看一个实际的例子:因为在设计中学生会转学院,所以,学生与学院是多对多的关系

1. 定义模型

class User(UserMixin, db.Model):  
    __tablename__ = 'users'  
    id = db.Column(db.Integer, primary_key=True)  
    email = db.Column(db.String(100), unique=True, index=True)  
      
    .............省略其他字段  
    departments=db.relationship('Department', secondary=user_department, backref=db.backref('users',lazy='dynamic'), lazy='dynamic')  
class Department(db.Model):  
    __tablename__ = 'departments'  
    id = db.Column(db.Integer, primary_key=True)  
    department = db.Column(db.String(100))
user_department = db.Table('user_department',  
    db.Column('user_id', db.Integer, db.ForeignKey('users.id'), primary_key=True),  
    db.Column('department_id', db.Integer, db.ForeignKey('departments.id'), primary_key=True)    
) 

2. 定义表单

class SmForm(Form):  
    name = StringField('真实姓名', validators=[Length(0, 64)])  
    ....................省略其他字段  
    is_departmentChange = BooleanField('是否转过学院')  
    pre_department = SelectField('原学院:', coerce=int)  
    cut_department = SelectField('现学院:', coerce=int)  
      
    submit = SubmitField('Submit')  
  
   #下拉菜单初始化  
    def __init__(self, user, *args, **kwargs):  
        super(SmForm, self).__init__(*args, **kwargs)  
        <strong>self.pre_department.choices = [(pre_department.id, pre_department.department)  
                             for pre_department in Department.query.order_by(Department.department).all()]  
        self.cut_department.choices = [(cut_department.id, cut_department.department)  
                             for cut_department in Department.query.order_by(Department.department).all()]</strong>  
         
        self.user = user 

3. 定义路由

@main.route('/sm', methods=['GET', 'POST'])  
@login_required  
@main.errorhandler(404)  
def sm():  
    user = User.query.filter_by(email=current_user.email).first()  
    form = SmForm(user)  
    if  user.is_realname ==False:  
        if form.validate_on_submit():  
  
            # User的学院更新  删除旧的数据,<strong>联合删除  
            usr = current_user._get_current_object()  
            deparment = user.departments.all()  
            for de in deparment:  
                de.users.remove(usr)</strong>  
  
       ........................省略其他  
  
              
            user.is_departmentChange = form.is_departmentChange.data  
          <strong>#向关系表中添加  
            user.departments.append(Department.query.get(form.pre_department.data))  
            user.departments.append(Department.query.get(form.cut_department.data))  
  
            db.session.add(user)  
            db.session.commit()</strong>  
            return redirect(url_for('.sm_success'))  
    return render_template('sm.html', form=form) 

4.渲染模板(省略)



SQLAlchemy_定义(一对一/一对多/多对多)关系

基本关系模式

from sqlalchemy import Table, Column, Integer, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

One To Many

表示一对多的关系时,在子表类中通过 foreign key (外键)引用父表类。
然后,在父表类中通过 relationship() 方法来引用子表的类:

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    children = relationship("Child")
   # 在父表类中通过 relationship() 方法来引用子表的类集合

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'))
    # 在子表类中通过` foreign key` (外键)引用父表的参考字段

在一对多的关系中建立双向的关系,这样的话在对方看来这就是一个多对一的关系
在子表类中附加一个 relationship() 方法,并且在双方的 relationship() 方法中使用relationship.back_populates 方法参数:

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    children = relationship("Child", back_populates="parent")

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'))
    parent = relationship("Parent", back_populates="children")
    # 子表类中附加一个 relationship() 方法
    # 并且在(父)子表类的 relationship() 方法中使用 relationship.back_populates 参数

这样的话子表将会在多对一的关系中获得父表的属性
或者,可以在单一的 relationship() 方法中使用 backref 参数来代替 back_populates 参数:

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    children = relationship("Child", backref="parent")

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'))

One To One

一对一是两张表之间本质上的双向关系
要做到这一点,只需要在一对多关系基础上的父表中使用 uselist 参数来表示
To convert one-to-many into one-to-one:

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    child = relationship("Child", uselist=False, back_populates="parent")

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'))
    parent = relationship("Parent", back_populates="child")

To convert many-to-one into one-to-one:

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    child_id = Column(Integer, ForeignKey('child.id'))
    child = relationship("Child", back_populates="parent")

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent = relationship("Parent", back_populates="child", uselist=False)

同样的,可以使用下面这种方式:

from sqlalchemy.orm import backref

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    child_id = Column(Integer, ForeignKey('child.id'))
    child = relationship("Child", backref=backref("parent", uselist=False))

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'))


Many To Many

多对多关系会在两个类之间增加一个关联的表。这个关联的表在 relationship() 方法中通过 secondary 参数来表示。
通常的,这个表会通过 MetaData 对象来与声明基类关联,所以这个 ForeignKey 指令会使用链接来定位到远程的表:

# 多对多关系中的两个表之间的一个关联表
association_table = Table('association', Base.metadata,
    Column('left_id', Integer, ForeignKey('left.id')),
    Column('right_id', Integer, ForeignKey('right.id'))
)

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)
    children = relationship("Child",
                    secondary=association_table)
    # 在父表中的 relationship() 方法传入 secondary 参数,其值为关联表的表名

class Child(Base):
    __tablename__ = 'right'
    id = Column(Integer, primary_key=True)

双向关系中,两个表类都会包含这个集合。指定使用 relationship.back_populates参数,并且为每一个 relationship()方法指定共用的关联表:

association_table = Table('association', Base.metadata,
    Column('left_id', Integer, ForeignKey('left.id')),
    Column('right_id', Integer, ForeignKey('right.id'))
)

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)
    children = relationship(
        "Child",
        secondary=association_table,
        back_populates="parents")

class Child(Base):
    __tablename__ = 'right'
    id = Column(Integer, primary_key=True)
    parents = relationship(
        "Parent",
        secondary=association_table,
        back_populates="children")

当在父表类的 relationship()方法中使用backref参数代替 relationship.back_populates 时,backref 会自动的为子表类加载同样的 secondary 参数。

association_table = Table('association', Base.metadata,
    Column('left_id', Integer, ForeignKey('left.id')),
    Column('right_id', Integer, ForeignKey('right.id'))
)

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)
    children = relationship("Child",
                    secondary=association_table,
                    backref="parents")

class Child(Base):
    __tablename__ = 'right'
    id = Column(Integer, primary_key=True)

secondary 参数还能够接收一个可调函数的最终返回值,

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)
    children = relationship("Child",
                    secondary=lambda: association_table,
                    backref="parents")
class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)
    children = relationship("Child",
                    secondary="association",
                    backref="parents")

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • 一、概念数据模型概述数据模型是现实世界中数据特征的抽象。数据模型应该满足三个方面的要求:1)能够比较真实地模拟现实...
    AFinalStone阅读 11,612评论 1 5
  • 今天是一个美好的一天,我们有个平台叫做思涵创业平台,我们每天认真参加三会,读书分享会,碰头会,小组会,必须认真去参...
    卓越成林爱编辑阅读 186评论 2 1
  • 平遥古城的那一天一夜,我是最孤独的,热闹的大街,我是孤独的,大街上的欢声笑语,我是孤独的,美丽的风景,我是...
    王神马阅读 143评论 0 0