【自动化运维新手村】Flask-ORM关联查询

【摘要】

到目前为止,Flask集成ORM扩展到基本操作,已经算是接近尾声了,上一章节已经将单表数据的增删改查,做了十分详细的讲解,并且从Flask应用的日志中可以看出每个ORM操作对应的数据库SQL语句,能够更为清晰的看到程序模型到数据库之间的映射关系,让大家可以对MySQL有一个基本的了解。

但几乎所有的后端应用都不可能只存在单独的一张数据表,大多数情况下都是存在多张数据表,并且这些数据表之间都存在关联,可能是一对一,或者一对多,等等。那么今天这一章节我们就着重讲解一下如何使用Flask-SQLAlchemy进行多表关联查询,并逐步完善后端应用的参数及异常处理。

【一/多 对 一/多】

数据库两张表之间的关系主要存在以下几种关系:

1.一对一

2.一对多

3.多对多

一对一

一对一是关系型数据库的两张表中较为普遍的映射关系,比如,设备信息表 - 设备详情表;

设备信息表中存储的设备基本信息包括ip, hostname, idc, row, column, vendor, model, role,那么一台设备出了具备这些基本信息外,可能还包含其他额外的信息,比如:资产号,最近一次启动时间,运行总时长,操作系统镜像版本,运行状态,过保时间,是否过保,Console口管理地址,IPv6管理地址,等等。

那么常用的做法就是将这些额外的信息单独建立一张设备详情表,一是避免原始表的数据列过多,二是基本信息和详细信息的查询频率也略有差异,并不是任何时候都需要将这些信息都查出来,所以建两张表是较为合适的做法。

在这里插入图片描述

如上图所示,略微调整了一下设备信息表,将部分字段放在了设备详情表中,并且在两张表中都增加sn(资产号)字段作为主键,来唯一标识一台设备,这是因为在堆叠交换机中,主备两台设备的IP是相同的,但资产号一直是可以保持唯一的。

所以将设备信息表和设备详情表通过资产号进行关联,形成一对一对关系。

一对多

一对多在关系型数据库中是最为普遍的映射关系,因为一对一在不考虑过滤数据库范式的情况下,可以将其合并成一张表。

一对多比较好理解的例子就是设备信息表与设备端口表之间的关系,设备信息表中的一行数据可以表示一台设备,而一台设备可以具有多个端口,这多个端口在端口表中存储为多行,所以两张表之间就形成了一对多关系,如下:

在这里插入图片描述

如上图所示,设备端口表中需要有一列资产号字段,最终数据的内容中,多行端口信息的sn可能相同,这个sn就可以与设备进行关联。

多对多

当两张表存在多对多关系时,通常的做法是额外新增一张中间表来进行关联,将一个多对多转换为两个多对一。

由于我们对后端应用中暂时没有多对多的场景,大家暂时只做初步的了解即可,如果十分感兴趣的朋友可以自行多做研究。

【Flask关联查询】

定义模型

class Devices(db.Model):
    __tablename__ = "devices"
    sn = db.Column(db.String(128), primary_key=True, comment="资产号")
    ip = db.Column(db.String(16), nullable=False, comment="IP地址")
    hostname = db.Column(db.String(128), nullable=False, comment="主机名")
    idc = db.Column(db.String(32), comment="机房")
    vendor = db.Column(db.String(16), comment="厂商")
    model = db.Column(db.String(16), comment="型号")
    role = db.Column(db.String(8), comment="角色")
    created_at = db.Column(db.DateTime(), nullable=False, server_default=text('NOW()'), comment="创建时间")
    updated_at = db.Column(db.DateTime(), nullable=False, server_default=text('NOW()'), server_onupdate=text('NOW()'), comment="修改时间")

class DeviceDetail(db.Model):
    __tablename = "device_detail"
    sn = db.Column(db.String(128), db.ForeignKey(Devices.sn), primary_key=True, comment="资产号")
    ipv6 = db.Column(db.String(16), nullable=False, comment="IPv6地址")
    console_ip = db.Column(db.String(16), nullable=False, comment="console地址")
    row = db.Column(db.String(8), comment="机柜行")
    column = db.Column(db.String(8), comment="机柜列")
    last_start = db.Column(db.DateTime(), comment="最近启动时间")
    runtime = db.Column(db.Integer, comment="运行时长")
    image_version = db.Column(db.String(128), comment="镜像版本")
    over_warrant = db.Column(db.BOOLEAN, comment="是否过保")
    warrant_time = db.Column(db.DateTime(), comment="过保时间")
    created_at = db.Column(db.DateTime(), nullable=False, server_default=text('NOW()'), comment="创建时间")
    updated_at = db.Column(db.DateTime(), nullable=False, server_default=text('NOW()'), server_onupdate=text('NOW()'), comment="修改时间")

class Ports(db.Model):
    __tablename = "ports"
    sn = db.Column(db.String(128), db.ForeignKey(Devices.sn), primary_key=True, comment="资产号")
    port_id = db.Column(db.String(16), nullable=False, comment="端口ID")
    port_name = db.Column(db.String(64), nullable=False, comment="端口名称")
    port_type = db.Column(db.String(16), comment="端口类型")
    bandwidth = db.Column(db.Integer, comment="端口速率")
    link_status = db.Column(db.String(8), comment="链路状态")
    admin_status = db.Column(db.String(8), comment="管理状态")
    interface_ip = db.Column(db.String(16), comment="端口IP")
    vlan_id = db.Column(db.String(8), comment="端口所属VLAN")
    created_at = db.Column(db.DateTime(), nullable=False, server_default=text('NOW()'), comment="创建时间")
    updated_at = db.Column(db.DateTime(), nullable=False, server_default=text('NOW()'), server_onupdate=text('NOW()'), comment="修改时间")

创建数据表

三张表的SQL语句如下:

CREATE TABLE devices (
        sn VARCHAR(128) NOT NULL COMMENT '资产号', 
        ip VARCHAR(16) NOT NULL COMMENT 'IP地址', 
        hostname VARCHAR(128) NOT NULL COMMENT '主机名', 
        idc VARCHAR(32) COMMENT '机房', 
        vendor VARCHAR(16) COMMENT '厂商', 
        model VARCHAR(16) COMMENT '型号', 
        `role` VARCHAR(8) COMMENT '角色', 
        created_at DATETIME NOT NULL COMMENT '创建时间' DEFAULT NOW(), 
        updated_at DATETIME NOT NULL COMMENT '修改时间' DEFAULT NOW(), 
        PRIMARY KEY (sn)
)
CREATE TABLE device_detail (
        sn VARCHAR(128) NOT NULL COMMENT '资产号', 
        ipv6 VARCHAR(16) NOT NULL COMMENT 'IPv6地址', 
        console_ip VARCHAR(16) NOT NULL COMMENT 'console地址', 
        `row` VARCHAR(8) COMMENT '机柜行', 
        `column` VARCHAR(8) COMMENT '机柜列', 
        last_start DATETIME COMMENT '最近启动时间', 
        runtime INTEGER COMMENT '运行时长', 
        image_version VARCHAR(128) COMMENT '镜像版本', 
        over_warrant BOOL COMMENT '是否过保', 
        warrant_time DATETIME COMMENT '过保时间', 
        created_at DATETIME NOT NULL COMMENT '创建时间' DEFAULT NOW(), 
        updated_at DATETIME NOT NULL COMMENT '修改时间' DEFAULT NOW(), 
        PRIMARY KEY (sn), 
        FOREIGN KEY(sn) REFERENCES devices (sn)
)
CREATE TABLE ports (
        sn VARCHAR(128) NOT NULL COMMENT '资产号', 
        port_id VARCHAR(16) NOT NULL COMMENT '端口ID', 
        port_name VARCHAR(64) NOT NULL COMMENT '端口名称', 
        port_type VARCHAR(16) COMMENT '端口类型', 
        bandwidth INTEGER COMMENT '端口速率', 
        link_status VARCHAR(8) COMMENT '链路状态', 
        admin_status VARCHAR(8) COMMENT '管理状态', 
        interface_ip VARCHAR(16) COMMENT '端口IP', 
        vlan_id VARCHAR(8) COMMENT '端口所属VLAN', 
        created_at DATETIME NOT NULL COMMENT '创建时间' DEFAULT NOW(), 
        updated_at DATETIME NOT NULL COMMENT '修改时间' DEFAULT NOW(), 
        PRIMARY KEY (sn, port_id), 
        FOREIGN KEY(sn) REFERENCES devices (sn)
)

一对一

通常在主表中定义relationship,在附表中定义外键,如下:

class Devices(db.Model):
    __tablename__ = "devices"
    ...
    detail = db.relationship("DeviceDetail", uselist=False, backref="device")

class DeviceDetail(db.Model):
    __tablename = "device_detail"
    sn = db.Column(db.String(128), db.ForeignKey(Devices.sn), primary_key=True, comment="资产号")
    ...

1.上述代码中的relationship,是关联属性的意思,是SQLAlchemy提供给开发者快速引用外键模型的一个对象属性,本身并不存在于MySQL中;

2.relationship的参数backref表示反向引用,通过外键模型查询主模型数据时的关联属性,通俗的讲就是在查DeviceDetail数据时,可以通过backref引用到Devices

3.useList表示关联模型是否为List,如果为False,则不使用列表,而使用标量值。一对一关系中,需要设置relationship中的uselist=Flase

一对多

通常在“一”表中定义relationship,在“多”表中定义外键

class Devices(db.Model):
    __tablename__ = "devices"
    ...
    ports = db.relationship("Ports", uselist=True, backref="device", lazy='dynamic')

1.由于Deviecs表和Ports表直接为一对多,通过Devices会关联查询到一个或多个端口记录,所以需要将useList设为True

2.参数backref可以在Ports中自动创建一个device属性,作为Devices的反向引用

3.参数lazy决定了ORM框架何时从数据库中加载数据:

lazy='subquery',查询当前数据模型时,采用子查询(subquery),把外键模型的属性也瞬间查询出来了。

lazy=Truelazy='select',查询当前数据模型时,不会把外键模型的数据查询出来,只有操作到外键关联属性时,才进行连表查询数据

lazy='dynamic',查询当前数据模型时,不会把外键模型的数据查询出来,只有操作到外键关联属性并操作外键模型具体属性时,才进行连表查询数据

【Flask改造】

统一给三个模型都加上to_dict()to_model()方法。

一对一获取设备详情的代码如下:

device = Devices.query.filter_by(sn=sn).first()
res = {**device, **device.detail.to_dict()}  # 通过device类的detail属性获取DeviceDetail的实例

上述代码中使用到了字典的一个小技巧,将多个字典合并可以使用{**dict1, dict2}

一对一添加设备详情的代码如下:

data = request.get_json()
device = Devices.to_model(**data)  # 生成Device模型实例
device.detail = DeviceDetail.to_model(**data)  # 生成DeviceDetail模型实例,并赋值给device对象
db.session.add(devices)  # 插入数据库
db.session.commit()  # 提交

在这里插入图片描述

一对多添加端口的代码如下:

def add_ports():
    data = request.get_json()
    if not isinstance(data, list):
        data = [data]
    sns = list(set([p.get("sn", "") for p in data]))  # 获取传入端口参数中的资产号,并去重
    devices = Devices.query.with_entities(Devices.sn).filter(Devices.sn.in_(sns)).all()  # 查询对应资产号的设备
    exists_sn = [d.sn for d in devices]  # 获取数据库中已存在的资产号
    try:
        ports = []
        for p in data:
            if p.get("sn", "") not in exists_sn:  # 如果端口所属的设备不存在,则返回错误
                return jsonify({"status_code": HTTPStatus.INTERNAL_SERVER_ERROR, "message": p.get("sn", "") + " device is not exists"})
            ports.append(Ports.to_model(**p))
        db.session.add_all(ports)
        db.session.commit()
        return jsonify({"status_code": HTTPStatus.OK})
    except Exception as e:
        return jsonify({"status_code": HTTPStatus.INTERNAL_SERVER_ERROR, "message": str(e)})

如代码中注释,需要在添加端口前判断是否已存在该端口所属的设备,如果设备不存在则应该直接返回错误,而实际上,即使不做这个判断,由于数据库中外键约束的存在,也会导致插入数据出错,但在接口编写时,应该遵循的原则是,将非法检查前置,避免压力集中在数据库上,这样有利于提高应用整体性能。
在这里插入图片描述

一对多获取设备端口的代码如下:

device = Devices.query.filter_by(sn=sn).first()
ports = [p.to_dict() for p in device.ports]  # 通过device类的ports属性获取Ports的实例
res = {**device.to_dict(), "ports": ports}

在这里插入图片描述

【总结】

这一章节我们对Flask-SQLAlchemy中关联查询的方法做了较为详细的讲解,并且从数据库层面分析了一对一,一对多等关系,除此之外还实现了一对一/多的查询和添加,其中使用到了一些较为Pythonic的语法和逻辑,需要大家慢慢消化。

最终整体的代码由于篇幅原因暂时就不放在文章中,如果有需要的朋友可以通过微信公众号加入读者交流群后获取。


欢迎大家添加我的个人公众号【Python玩转自动化运维】加入读者交流群,获取更多干货内容

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值