【自动化运维新手村】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=True或lazy='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玩轉自動化運維】加入讀者交流群,獲取更多干貨內容
總結
以上是生活随笔為你收集整理的【自动化运维新手村】Flask-ORM关联查询的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 图片的绘画器
- 下一篇: (病毒安全)服务器被中了木马,如何清除