概述
我们已经实现了产品管理页面的基础功能,接下来我们需要实现在产品项目下,实现创建应用管关联的功能,主要是用于关联创建的项目,例如当你有一个项目需求开始提测了,此时你就可以创建一个基础提测应用,关联你提测的项目
- 平台功能
- 服务器端python
- 使用DBUntil库优化数据库的连接
- 新增配置文件,优化代码逻辑
- 格式化返回结果,以及多条件查询代码实现
- 数据库mysql
- 应用管理表创建(外键关联)
- 联合表查询回顾
- 使用LIMIT语法做数据分页
- 前端vue
- 下拉筛选控件(select)的基本和自定义用法
- 表格分页控件Pagination实现多数据分页
- 服务器端python
DBUntils连接池
在项目中链接数据是直接通过pymysql去做的链接请求关闭,每次操作都要独立重复请求,其实是比较浪费资源,在并发不大的小项目虽然无感知,但如果有频繁请求的项目中,就会有性能问题,那么可以通过使用连接池技术,管理来进行优化,DBUnitls是一套Python的数据库连接池包,对链接对象进行自动链接和释放,提高高频高并发下数据访问性能,大概的原理如:
DBUntils 按照配置初始化多个数据库连接存储在连接池中
在程序创建连接的时候,从空闲的连接池中获取,不需要重新初始化连接,提升连接速度;
在程序使用完毕后,把连接放回连接池,并不真正的关闭,等待其他请求使用,减少频繁数据的打开和关闭操作。
DBUntils 提供两种方式,并都能自动管理
PersistentDB (persistent_db) 提供线程专用的连接
PooledDB (pooled_db) 提供线程间可共享的连接
使用此功能需要安装依赖
pip install DBUtils
一般比较常用就是共享的方式,以 pooled_db为例子,使用方法很简单
import pymysql
from dbutils.pooled_db import PooledDB
# 初始化数据库连接,使用pymysql连接,
pool = PooledDB(pymysql,3,host='',port='',user='',passwd='',database='')
# 一般连接
db = pool.connection()
cur = db.cursor()
cur.execute(...)
res = cur.fetchone()
cur.close() # 关闭使用游标
db.close() # 关闭使用连接
# 或者通过with方式
with pool.connection() as db:
with db.cursor as cur:
cur.execute(...)
res = cur.fetchone()
一些参数说明,上边说明中第一参数为 creator 指定那种连接模式,第二参数为mincached
mincached :启动时开启的空的连接数量
maxcached :连接池最大可用连接数量
maxshared : 连接池最大可用共享连接数量
maxconnections : 最大允许连接数量
maxusage :单个丽娜姐最大复用次数
blocking :达到最大数量时是否阻塞
这里只是对连接的管理,DBUntils的基本知识点讲完了,关于性能对比可以网上搜索相关内容,另外其他的数据库操作还按照自己使用方法进行操作即可。
SQL中limit用法
项目中对于较多的数据显示,需要实现分页数据查询,这可以利用SQL语法关键词 limit ,它可以限制查询结果返回的数量,也可以指定起始索引,来完成分页查询效果,语法形式
# 基本用法
SELECT * FROM table LIMIT [offset,] rows | rows OFFSET offset
# 简化形式
select * from tableName limit i,n
i:指定第一个返回记录行的偏移量(可省略即表示从0开始)
n:第二个参数指定返回记录行的最大数目
两个示例如:
... limit 10 # 查询前10条数据, 也可以表示为limit 0,10
... limit 10,15 # 从11条开始返回后15条数据 即11-25
这里有个注意事项,网上好多关于limit最后一个参数用 -1 可以表示,指定偏移量到最后,这个目前在新的MYSQL版本已经无效,会报语法错误,办法是用个较大的数代替。
如何实现分页查询,通过观察我们可以这样,如果前端传递的page页数(初始为1),每页的数量为page_size,那么第一个参数计算偏移量可设置为 (page-1) * page_size,第二参数表可设置为page_size, 例如 前端请求第一页20个,则计算后后两个参数为( 0, 20),再如请求第2页20个,则计算后两个参数为 ( 20, 20 ),这样就可以实现分页数据查询效果。
这个知识点最后,扩展一个小优化,如果是分页数据特别,第一个参数偏移量太大的时候会带来性能的问题,优化的方式是使用子查询优化(前提条件是有自增ID的表),即先通过查询偏移量ID,然后条件 “>=ID + limit 数量" 进行查询
SELECT * FROM apps where id >=(SELECT id FROM apps LIMIT 100000,1) LIMIT 20
前端组件
分页 Pagination
当数据量过多时,使用分页分解数据。[注解-2]
基本样式和用法
设置layout,表示需要显示的内容,用逗号分隔,布局元素会依次显示。prev表示上一页,next为下一页,pager表示页码列表,除此以外还提供了->后的元素会靠右显示,jumper表示跳页元素,total表示总条目数,sizes表示每页显示条数,size用于设置每页显示的页码数量(默认10),通过pager-count属性可以设置最大页码按钮数(默认大于7页的时候6页后...缩略 ),使用了size-change和 current-change事件来处理页码大小和当前页变动时候触发的事件。
选择器 Select
当选项过多时,使用下拉菜单展示并选择内容。[注解-3]
基本用法 :<el-select>控件标记,v-model绑定数据,<el-option> 选项集。
<el-select v-model="value" placeholder="请选择">
<el-option key="key1" label="选项1" :value="key1"/>
<el-option key="key2" label="选项2" :value="key2"/>
</el-select>
自定义用法:<span style="float: left/right">显示自定义的左右内容
<el-select v-model="value" placeholder="请选择">
<el-option
v-for="item in cities"
:key="item.value"
:label="item.label"
:value="item.value">
<span style="float: left">{{ item.label }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.value }}</span>
</el-option>
</el-select>
具体使用和其他样式参数参考官方
以上是实现“应用管理”模块显示功能中新用到的两个组件,对于要实现分页,前端请求数据需要给 页码current-page和 页数page_size ,后端数据还要返回一个总量total数据。
模糊查询与分页功能实现
通过上边的新增知识点,进行实际的应用实现
功能实现(步骤)伪代码
根据需求罗列所需字段创建 apps应用数据库表,并插入一些测试数据大于10条以上;
后端创建一个配置文件管理数据相关字段信息;
后端创建application.py 用来实现DBUntils连接和应用管理api;
接口返回的Response抽取出来定一个通用json格式返回;
GET 实现/api/application/product 归属分类(products表)查询,只需要 ;id,keyCode,title 数据;
POST 实现/api/application/search 多关关键词(apps表)查询,同时要根据前端传的分页;和数量进行分页查询,并要返回个表总数;
前端menu菜单实现新目录结构
创建新的api请求,实现对后端提供的接口请求
前端查询产品,并使用自定义展示数据,选择数据后可清除,其他搜索条件参照以往input用法;
使用Table组件绑定数据,使用Pagination 绑定分页数据和实现切换时候动作方法,样式使用带背景,总数,数量选择,页码,以及上、下页切换。
实践参考实现
1.1 数据库apps表创建
DROP TABLE IF EXISTS `apps`;
CREATE TABLE `apps` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '自增id',
`appId` varchar(50) DEFAULT NULL COMMENT '应用服务ID',
`productId` bigint DEFAULT NULL COMMENT '外键关联产品所属',
`note` varchar(100) DEFAULT NULL COMMENT '应用描述',
`tester` varchar(300) DEFAULT NULL COMMENT '测试负责人',
`developer` varchar(300) DEFAULT NULL COMMENT '默认研发负责人',
`producer` varchar(300) DEFAULT NULL COMMENT '默认产品经理',
`CcEmail` varchar(500) DEFAULT NULL COMMENT '默认抄送邮件或组',
`gitCode` varchar(200) DEFAULT NULL COMMENT '代码地址',
`wiki` varchar(200) DEFAULT NULL COMMENT '项目说明地址',
`more` text COMMENT '更多的信息',
`status` char(1) DEFAULT '0' COMMENT '状态0正常1删除',
`creteUser` varchar(20) DEFAULT NULL COMMENT '创建人',
`createDate` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updateUser` varchar(20) DEFAULT NULL COMMENT '修改人',
`updateDate` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `apps_id_uindex` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10014 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='应用管理';
创建一个常量配置
目录/configs/config.py
# config.py
# 数据库配置
MYSQL_HOST= 'localhost'
MYSQL_PORT = 3306
MYSQL_DATABASE = 'tmpdatas'
MYSQL_USER = 'root'
MYSQL_PASSWORD = 'xxxx'
创建新的蓝图api应用管理模块,实现DBUntils初始化连接
路径 /apis/application.py ,并导入config数据库配置,以及dbutils模块
from flask import Blueprint
from dbutils.pooled_db import PooledDB
from configs import config
import pymysql.cursors
# 使用数据库连接池的方式链接数据库,提高资源利用率
pool = PooledDB(pymysql, mincached=2, maxcached=5,host=config.MYSQL_HOST, port=config.MYSQL_PORT,
user=config.MYSQL_USER, passwd= config.MYSQL_PASSWORD, database=config.MYSQL_DATABASE,
cursorclass=pymysql.cursors.DictCursor)
app_application = Blueprint("app_application", __name__)
创建一个统一格式类
目录/configs/ 添加一个统一Response返回
# format.py
resp_format_success = {
"code": 20000,
"message": "success",
"data": [],
"total": 0
}
实现所属分类查询用于条件筛选
@app_application.route("/api/application/product",methods=['GET'])
def getProduct():
# 使用连接池链接数据库
connection = pool.connection()
with connection.cursor() as cursor:
# 查询产品信息表-按更新时间新旧排序
sql = "SELECT id,keyCode,title FROM `products` WHERE `status`=0 ORDER BY `update` DESC"
cursor.execute(sql)
data = cursor.fetchall()
# 按返回模版格式进行json结果返回
response = format.resp_format_success
response['data'] = data
return response
实现分页查询应用
需要对查询条件进行按需拼接,还需要通过两次查询,一次查询总数量,一次是limit分页查询
@app_application.route("/api/application/search", methods=['POST'])
def searchBykey():
try:
body = request.get_json()
if not body:
return format.resp_format_fail
# 获取分页参数并验证
try:
pageSize = int(body.get('pageSize', 10))
currentPage = int(body.get('currentPage', 1))
except (ValueError, TypeError):
pageSize = 10
currentPage = 1
# 确保分页参数合理
pageSize = max(1, min(pageSize, 100)) # 限制每页最多100条
currentPage = max(1, currentPage)
# 基础语句和参数
base_sql = " WHERE A.`status`=0 AND A.productId = P.id"
count_sql = "SELECT COUNT(*) as `count` FROM `apps` AS A, `products` AS P WHERE A.`status`=0 AND A.productId = P.id"
data_sql = "SELECT P.title, A.* FROM apps AS A, products AS P WHERE A.`status`=0 AND A.productId = P.id"
params = []
# 拼接查询条件
conditions = []
if 'productId' in body and body['productId']:
conditions.append("A.`productId` = %s")
params.append(body['productId'])
if 'appId' in body and body['appId']:
conditions.append("A.`appId` LIKE %s")
params.append('%' + body['appId'] + '%')
if 'note' in body and body['note']:
conditions.append("A.`note` LIKE %s")
params.append('%' + body['note'] + '%')
if 'tester' in body and body['tester']:
conditions.append("A.`tester` LIKE %s")
params.append('%' + body['tester'] + '%')
if 'developer' in body and body['developer']:
conditions.append("A.`developer` LIKE %s")
params.append('%' + body['developer'] + '%')
if 'producer' in body and body['producer']:
conditions.append("A.`producer` LIKE %s")
params.append('%' + body['producer'] + '%')
# 如果有条件,添加到SQL中
if conditions:
condition_str = " AND " + " AND ".join(conditions)
base_sql += condition_str
count_sql += condition_str
data_sql += condition_str
# 排序和分页
data_sql += " ORDER BY A.`updateDate` DESC LIMIT %s, %s"
offset = (currentPage - 1) * pageSize
data_params = params + [offset, pageSize]
# 使用连接池链接数据库
connection = pool.connection()
with connection:
# 先查询总数
with connection.cursor() as cursor:
cursor.execute(count_sql, params)
total_result = cursor.fetchone()
total = total_result['count'] if total_result else 0
# 执行查询
with connection.cursor() as cursor:
cursor.execute(data_sql, data_params)
data = cursor.fetchall()
# 按分页模版返回查询数据
response = format.resp_format_success
response['data'] = data
response['total'] = total
return response
except Exception as e:
# 记录错误日志
# app.logger.error(f"Search error: {str(e)}")
response = format.resp_format_fail
response['message'] = "服务器内部错误"
return response, 500
接口文件全部实现完可不要忘记app.py文件中添加注册
from apis.application import app_application
....
app.register_blueprint(app_application)
vue前端对左侧目录进行重新整理
路径 /src/router/index.js 变更,其中涉及到子菜单用法,用children,这没有而外讲,直接照着代码一看就明白了。
{
path: '/settings',
component: Layout,
redirect: '/settings',
meta: { title: '基础管理', icon: 'dashboard' },
children: [{
path: 'product',
name: 'Product',
component: () => import('@/views/product/product'),
meta: { title: '项目产品分类', icon: 'dashboard' }
},
{
path: 'apps',
name: 'apps',
component: () => import('@/views/product/apps'),
meta: { title: '服务应用管理', icon: 'dashboard' }
}
]
},
创建应用页面的api请求文件和方法
目录/src/api/apps.js 组装产品列表和应用搜索接口请求
import request from '@/utils/request'
// 应用搜索接口
export function apiAppsSearch(requestBody) {
return request({
url: '/api/application/search',
method: 'post',
data: requestBody
})
}
// 产品选择项目列表
export function apiAppsProduct() {
return request({
url: '/api/application/product',
method: 'get'
})
}
实现搜索区域功能
先要创建app管理页面,路径/src/views/product/apps.vue ,然后分别利用上边的知识点实现自定义选择框的绑定,和其他input字段的绑定,另外有个添加 按钮(暂不实现)占位,html区域如下,js内容最后统一给出。
<div class="filter-container">
<el-form :inline="true" :model="search">
<el-form-item label="归属分类">
<el-select v-model="search.productId" filterable="true" clearable>
<el-option value="" label="所有"></el-option>
<el-option
v-for="item in options"
:key="item.id"
:label="item.title"
:value="item.id">
<span style="float: left">{{ item.keyCode }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.title }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="应用ID">
<el-input v-model="search.appId" placeholder="服务ID关键词" style="width: 200px;" clearable/>
</el-form-item>
<el-form-item label="描 述">
<el-input v-model="search.note" placeholder="描述模糊搜索" style="width: 200px;" clearable/>
</el-form-item>
<br>
<el-form-item label="研 发">
<el-input v-model="search.developer" placeholder="默认研发" style="width: 210px;" clearable/>
</el-form-item>
<el-form-item label="产 品">
<el-input v-model="search.producer" placeholder="默认产品" style="width: 210px;" clearable/>
</el-form-item>
<el-form-item label="测 试">
<el-input v-model="search.tester" placeholder="默认测试" style="width: 210px;" clearable/>
</el-form-item>
<el-form-item>
<el-button type="primary" plain @click="searchClick()">搜索</el-button>
</el-form-item>
</el-form>
<el-button type="primary" icon="el-icon-plus" style="float:right" @click="addApp()">添加应用</el-button>
</div>
10.1 表格数据的绑定
历史table的内容,可按照产品页实现来,这里注意依然有时间格式的问题,另外还需要添加个操作列是实现 编辑 功能(暂不实现)占位。
<el-table :data="tableData">
<!--:data prop绑定{}中的key,label为自定义显示的列表头-->
<el-table-column prop="appId" label="应用ID"/>
<el-table-column prop="note" label="描述" show-overflow-tooltip/>
<el-table-column prop="title" label="归属分类"/>
<el-table-column prop="developer" label="默认研发" />
<el-table-column prop="producer" label="默认产品"/>
<el-table-column prop="tester" label="默认测试"/>
<el-table-column prop="updateUser" label="更新人"/>
<el-table-column :formatter="formatDate" prop="updateDate" label="更新时间"/>
<el-table-column label="操作">
<template slot-scope="scope">
<el-link icon="el-icon-edit" @click="updateApp(scope.row)">修改</el-link>
</template>
</el-table-column>
</el-table>
10.2 表格下方的分页控件实现
带背景的页码,以及其他上下页,总数,自定义数量选择。
<div>
<el-pagination
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="search.currentPage"
:page-size="search.pageSize"
layout="total, sizes, prev, pager, next"
:page-sizes="[5, 10, 20, 30, 50]"
:total=total>
</el-pagination>
</div>
最后所有 script 部分的代码如下
<script>
import moment from 'moment'
import { apiAppsProduct, apiAppsSearch } from '@/api/apps'
export default {
name: 'Apps',
data() {
return {
search: {
productId: '',
appId: '',
note: '',
developer: '',
producer: '',
tester: '',
pageSize: 10,
currentPage: 1
},
options: [],
total: 0,
tableData: []
}
},
// 页面生命周期中的创建阶段调用
created() {
// 调用methods的方法,即初次加载就请求数据
this.productList()
this.searchClick()
},
methods: {
formatDate(row, column) {
const date = row[column.property]
if (date === undefined) {
return ''
}
// 使用moment格式化时间,由于我的数据库是默认时区,偏移量设置0,各自根据情况进行配置
return moment(date).utcOffset(0).format('YYYY-MM-DD HH:mm')
},
productList() {
apiAppsProduct().then(resp => {
this.options = resp.data
})
},
searchClick() {
apiAppsSearch(this.search).then(response => {
// 将返回的结果赋值给表格自动匹配
this.tableData = response.data
this.total = response.total
})
},
handleSizeChange(val) {
// console.log(`每页 ${val} 条`)
this.search.pageSize = val
this.searchClick()
},
handleCurrentChange(val) {
// console.log(`当前页: ${val}`)
this.search.currentPage = val
this.searchClick()
},
addApp() {
this.$message({
message: '我是待实现的CASE',
type: 'warning'
})
},
updateApp() {
this.$message({
message: '我是待实现的CASE',
type: 'warning'
})
}
}
}
</script>
分别启动前后的页面实现效果如下: