整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

JS写的服务端,如何连接Mysql数据库,实现增删查改功能

一篇文章我给大家说明了如何从零开始搭建一个node的服务端框架,我们用到了Egg框架。Egg框架我不再过多介绍,如果有小伙伴想了解,可以回去看我以前写的文章,会有相关的介绍。这次我将在上次搭建的框架上进行延伸,讲一下如果用Egg框架连接数据库,并且实现对数据的增删查改。接下来我们直接进入主题。

安装数据库插件

我本次选用的数据库是MySQL。所以我们安装Egg官方的数据库插件即可,首先我们安装插件 egg-mysql我们在项目根目录打开命令提示符,输入命令行:npm i --save egg-mysql 。回车等待插件下载安装完成。

npm i --save egg-mysql

配置插件

命令行下载安装插件完成后,我们下一步的工作就是在项目中开启并配置egg-mysql插件。具体操作如下:

首先我们要在项目中开启数据库。找到项目中的/config/plugin.js文件我们需要在里面添加几行代码,如下所示。

//开启数据库插件
  mysql : {
    enable: true,
    package: 'egg-mysql',
  }

然后我们还要在 config/config.default.js 中配置各个环境的数据库连接信息。具体配置如下。

//添加数据库连接信息
  config.mysql = {
    // 单数据库信息配置
    client: {
      // host
      host: 'localhost',
      // 端口号
      port: '3306',
      // 用户名
      user: 'root',
      // 密码
      password: '123456',
      // 数据库名
      database: 'testdb',
    },
    // 是否加载到 app 上,默认开启
    app: true,
    // 是否加载到 agent 上,默认关闭
    agent: false,
  };

到此步骤我们的数据库插件已经安装完成并且配置好了。那我们怎么实现数据的增删查改呢?大家请继续往下看。

数据操作-新增用户

首先我们看一下怎么新增数据。我们在mysql的testdb实例中新建一个user空表。如下图所示。

我们的egg框架也遵循MVC的架构所以我们一般会在service层里面写我们逻辑处理的代码,而controller层则是获取前端数据,回传数据的控制层。所以我们操作数据库的代码是写在service文件夹里面的。

我们在app/service文件夹里面新建一个user.js文件。在里面写个新增用户的方法,该方法就是把数据存到数据库中。具体代码如下。

const Service = require('egg').Service;

class UserService extends Service {

  //新增用户data是有controller层传递过来的数据记录。
  async addUser(data) {

    const {ctx, app} = this;
    let result = {};
    try {
      data.id = 0;//定义id=0,因为数据库已经设置id为主键,并且自增。所以只需要赋值0即可。
      // 在 user 表中,插入前端提交上来的数据记录
      const info = await app.mysql.insert('user', data); 
  
      //插入成功后。
      if(info.affectedRows === 1){
        //给前端返回一个Json的对象
        result = {
          state: 0, //自定义的状态码
          msg: "添加成功", //返回的消息
          data: info.insertId, //新增的记录的id
        }
      }

    } catch (err) {
      //插入数据失败的返回结果
      result = {
        state: 1, 
        msg: err,
        data: null,
      }
    }
    
    return result
  }
};
module.exports = UserService;

然后我们在app/controller文件夹里新建一个user.js文件。在这里我们需要获取前端提交上来的数据,并且将数据处理的结果返回给前端。具体代码如下。

'use strict';

const Controller = require('egg').Controller;
/**
 * @Controller 用户管理
 */
class UserController extends Controller {

  /**
  * @summary 新增用户
  * @router post /user/add
  * @request body userAddRequest 
  * @response 200 
  */
  async addUser() {
    const { ctx } = this;

    //通过ctx.request.body的方式,可以获取到前端post方式提交上来的数据
    const data = ctx.request.body;

    //调用service层的addUser方法。并且返回相应的结果
    const userInfo = await ctx.service.user.addUser(data);
    
    //向前端接口响应数据。
    ctx.body = userInfo;
  }

}

module.exports = UserController;

最后我们定义一个路由,让前端请求访问此路由。框架会监听路由是否被访问,如果被访问了则会调用我们定义在controller层的新增用户的方法。我们在app/router.js文件中添加如下代码,即可完成路由的定义。

//新增用户路由
  router.post('/user/add', controller.user.addUser);

完成这步骤后,我们一个新增用户的功能就已经完成了。接下里我们就测试一下它的实际效果。我们运行命令:npm run dev。启动项目,然后打开网页http://127.0.0.1:7001,可以直接在swagger-ui.html页面中进行测试。结果如下图所示。

经过测试,数据已经添加完成。所以数据库连接也是正常的。

本次分享暂时先告一段落。请各位小伙伴抬起你们发财的小手,点个赞呗。下次我将会进行和大家分享对数据查改删的方法。关注我!!!更多精彩分享不迷路。

日客户要求表内的数据依据某种分组生成HTML页面进行展示,一般处理这种需求直接上编程工具就好了,从数据库里读取数据,根据规则生成字符串,最后将字符串写出到文件。由于需求比较急,作为数据库编程系列文章,如果能用SQL实现首选还是SQL,这样处理既直接又快速,不过针对SQL要真的有耐心和信心写完,调试更是崩溃。由于要写出文件到硬盘,最后还是选择MySQL作为数据库工具,Navicat作为开发工具。

有两张表计划表、市县表,二者依靠市县编码(sxbm)进行等值连接,计划表内含有各个学校投放在各个市县的专业代号(zydh),专业名称(zymc)、招生备注(bz)、学制(xz)、要求的学历(xl)、计划数(jh)等字段组成的计划信息,院校编码(yxbm)为学校的两位数编码,院校代号(yxdh)为院校编码(yxbm)+市县编码(sxbm)组成的四位数编码,院校代号其实可以区分出学校在哪个市县的投档的专业计划。要求以学校为单位创建HTML页面,页面首先要以市县作为表格分割,然后根据专业代号排序。具体实现过程如下:

创建计划表:

CREATE TABLE `zzjh2019v` (
`YXDH` varchar(9) COMMENT '学校代号',
`YXMC` varchar(54) COMMENT '学校名称',
`ZYDH` varchar(2) COMMENT '专业代号',
`ZYMC` varchar(28) COMMENT '专业名称',
`XZ` varchar(3) COMMENT '学制',
`XL` varchar(4) COMMENT '学历',
`JH` varchar(6) COMMENT '招生计划数',
`BZ` varchar(200) COMMENT '备注',
`yxbm` char(2) COMMENT '学校编码',
`sxbm` char(2) COMMENT '市县编码'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Compact;

创建市县编码表:

CREATE TABLE `sx` (
`sxbm` char(2) COMMENT '市县编码',
`sxmc` varchar(20) COMMENT '市县名称'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Compact;

纠结了很久这个东西怎么写,最后采取游标、拼接字符串、字符串聚合,动态SQL,写文件等一些列操作完成需求,创建的存储过程如下:

CREATE DEFINER=`root`@`localhost` PROCEDURE `splitjh`()
BEGIN
declare done INT DEFAULT 0;
declare pyxbm char(2);
declare psxmc varchar(10);
declare pyxmc varchar(50);
declare pjhall int;
declare pjhrows TEXT;
declare yxjh cursor
for
select yxbm,yxmc,sum(jh) jhall from zzjh2019v a,sx b where a.sxbm=b.sxbm group by yxbm,yxmc order by yxbm;
declare CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;
open yxjh;
fetch yxjh into pyxbm,pyxmc,pjhall;
while done !=1 do
select group_concat(jhrow separator '') into pjhrows from
(select concat('<tr class="subtitle"><td>',yxdh,'</td><td>',yxmc,'在 <span><font color="red">',b.sxmc,'</font></span> 招生计划如下</td><td>',sum(jh),'</td><td></td><td></td></tr>',group_concat('<tr class="jhrow"><td>',zydh,'</td><td>',zymc,'(',bz,')</td><td>',jh,'</td><td>',xz,'</td><td>',xl,'</td></tr>' order by zydh separator '')) jhrow
from zzjh2019v a,sx b where yxbm=pyxbm and a.sxbm=b.sxbm group by yxdh order by yxdh,zydh) jhs;
set @pfilename = concat('''d:/32/1/1/jh11',pyxbm,'.html''');
set @sql =concat('select concat(''<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><link rel="stylesheet" type="text/css" href="zsjh.css" ><title>3+2计划</title></head><body><h3></h3><table><tr class="subtitle"><th>代号</th><th>专业及名称备注</th><th>人数</th><th>学制</th><th>学历</th></tr>'',''',pjhrows,''',''</body></html>'') from dual into outfile ',@pfilename);
prepare execsql from @sql;
execute execsql;
DEALLOCATE PREPARE execsql;
fetch yxjh into pyxbm,pyxmc,pjhall;
end while;
close yxjh;
END;

首先看效果,执行过程

call splitjh();

在磁盘形成的HTML文件效果如下图(数据有一定的敏感性,进行了遮挡处理):

文件展示页面

生成的文件列表如下图:

生成的文件列表

这里一共有87所学校,所以生成了87的文件,添加CSS样式文件,让表格呈现如前图所示。

技术点

1)MySQL的游标,以及循环读取游标的方法,涉及的语句如下:

declare yxjh cursor
for
select yxbm,yxmc,sum(jh) jhall from zzjh2019v a,sx b where a.sxbm=b.sxbm group by yxbm,yxmc order by yxbm;#游标定义
declare CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;#游标循环条件,注意此句一定要定义在游标之后,才起作用
open yxjh;#打开游标
fetch yxjh into pyxbm,pyxmc,pjhall;#将游标行内容赋值给变量。

2)执行动态SQL,由于MySQL into outfile 后接的文件名不能为变量,所以必须使用动态SQL的方法,涉及的语句如下:

prepare execsql from @sql;#从一个变量准备一个动态sql,注意execsql不用提前定义
execute execsql;#执行准备好的语句
DEALLOCATE PREPARE execsql;#销毁语句

综上就是使用MySQL数据库,并借用MySQL写文件的方式将数据从数据库内按照需求导出文件,为何不用navicat导出呢?因为无法达到要求,又是聚合、又是格式,所以只能自己编写过程通过SQL语句拼接字符串的方式来实现。没有太多的技术难度,主要是想法和调试难度。后续在此基础上又开发了以市县为单位创建HTML文件,各招生学校作为分割的过程。本案例是实际需求催生出来的做法,在遇到这样的需求前你是先想到SQL还是先想到开发工具呢?从实际效果看使用SQL这种方式更加灵活。这样的SQL实现的字符串拼接是不是有点极限呢?

数据库已经越来越被人们熟知,同时也在许多企业中得到了应用,但是由于市面上没有统一的图查询语言标准,所以有部分开发者对于不同图数据库的用法存在着疑问。因此本文作者对市面上主流的几款图数据库进行了一番分析,并以查询操作为例进行深入介绍。

文章的开头我们先来看下什么是图数据库,根据维基百科的定义:图数据库是使用图结构进行语义查询的数据库,它使用节点、边和属性来表示和存储数据

虽然和关系型数据库存储的结构不同(关系型数据库为表结构,图数据库为图结构),但不计各自的性能问题,关系型数据库可以通过递归查询或者组合其他 SQL 语句(Join)完成图查询语言查询节点关系操作。得益于 1987 年 SQL 成为国际标准化组织(ISO)标准,关系型数据库行业得到了很好的发展。同 60、70 年代的关系型数据库类似,图数据库这个领域的查询语言目前也没有统一标准,虽然 19 年 9 月经过国际 SQL 标准委员会投票表决,决定将图查询语言(Graph Query Language)纳为一种新的数据库查询语言,但 GQL 的制定仍需要一段时间。

鉴于市面上没有统一的图查询语言标准,在本文中我们选取市面上主流的几款图查询语言来分析一波用法,由于篇幅原因本文旨在简单介绍图查询语言和常规用法,更详细的内容将在进阶篇中讲述。

图查询语言·介绍

图查询语言 Gremlin

Gremlin 是 Apache ThinkerPop 框架下的图遍历语言。Gremlin 可以是的也可以是命令性的。虽然 Gremlin 是基于 Groovy 的,但具有许多语言变体,允许开发人员以 Java、JavaScript、Python、Scala、Clojure 和 Groovy 等许多现代编程语言原生编写 Gremlin 查询

支持图数据库:Janus Graph、InfiniteGraph、Cosmos DB、DataStax Enterprise(5.0+) 、Amazon Neptune

图查询语言 Cypher

Cypher 是一个描述性的图形查询语言,允许不必编写图形结构的遍历代码对图形存储有表现力和效率的查询,和 SQL 很相似,Cypher 语言的关键字不区分大小写,但是属性值,标签,关系类型和变量是区分大小写的。

支持图数据库: Neo4j、RedisGraph、AgensGraph

图查询语言 nGQL

nGQL 是一种类 SQL 的声明型的文本查询语言,nGQL 同样是关键词大小写不敏感的查询语言,目前支持模式匹配、聚合运算、图计算,可无嵌入组合语句。

支持图数据库:Nebula Graph

图查询语言·术语篇

在比较这 3 个图查询语言之前,我们先来看看他们各自的术语,如果你翻阅他们的文档会经常见到下面这些“关键字”,在这里我们不讲用法,只看这些图数据库常用概念在这 3 个图数据库文档中的叫法。

术语GremlinCyphernGQL点VertexNodeVertex边EdgeRelationshipEdge点类型LabelLabelTag边类型labelRelationshipTypeedge type点 IDvidid(n)vid边 IDeidid®无插入addcreateinsert删除dropdeletedelete / drop更新属性setPropertysetupdate

我们可以看到大体上对点和边的叫法类似,只不过 Cypher 中直接使用了 Relationship 关系一词代表边。其他的术语基本都非常直观。

图查询语言·实操篇

上面说了一通术语之类的“干货”之后,是时候展示真正的技术了——来个具体一点的例子,在具体的例子中我们将会分析 Gremlin、Cypher、nGQL 的用法不同。

示例图:The Graphs of Gods

实操示例使用了 Janus Graph 的示例图 The Graphs of Gods。该图结构如下图所示,描述了罗马万神话中诸神关系。

插入数据

复制代码

# 插入点## nGQLnebula> INSERT VERTEX character(name, age, type) VALUES hash("saturn"):("saturn", 10000, "titan"), hash("jupiter"):("jupiter", 5000, "god");## Gremlingremlin> saturn = g.addV("character").property(T.id, 1).property('name', 'saturn').property('age', 10000).property('type', 'titan').next();==>v[1]gremlin> jupiter = g.addV("character").property(T.id, 2).property('name', 'jupiter').property('age', 5000).property('type', 'god').next();==>v[2]gremlin> prometheus = g.addV("character").property(T.id, 31).property('name',  'prometheus').property('age', 1000).property('type', 'god').next();==>v[31]gremlin> jesus = g.addV("character").property(T.id, 32).property('name',  'jesus').property('age', 5000).property('type', 'god').next();==>v[32]## Cyphercypher> CREATE (src:character {name:"saturn", age: 10000, type:"titan"})cypher> CREATE (dst:character {name:"jupiter", age: 5000, type:"god"})# 插入边## nGQLnebula> INSERT EDGE father() VALUES hash("jupiter")->hash("saturn"):();## Gremlingremlin> g.addE("father").from(jupiter).to(saturn).property(T.id, 13);==>e[13][2-father->1]## Cyphercypher> CREATE (src)-[rel:father]->(dst)

在数据插入这块,我们可以看到 nGQL 使用 INSERT VERTEX 插入点,而 Gremlin 直接使用类函数的 g.addV() 来插入点,Cypher 使用 CREATE 这个 SQL 常见关键词来创建插入的点。在点对应的属性值方面,nGQL 通过 VALUES 关键词来赋值,Gremlin 则通过操作 .property() 进行对应属性的赋值,Cypher 更直观直接在对应的属性值后面跟上想对应的值。

在边插入方面,可以看到和点的使用语法类似,只不过在 Cypher 和 nGQL 中分别使用 -[]-> 和 **-> 来表示关系,而 Gremlin 则用 to() ** 关键词来标识指向关系,在使用这 3 种图查询语言的图数据库中的边均为有向边,下图左边为有向边,右边为无向边。

删除数据

复制代码

# nGQLnebula> DELETE VERTEX hash("prometheus");# Gremlingremlin> g.V(prometheus).drop();# Cyphercypher> MATCH (n:character {name:"prometheus"}) DETACH DELETE n 

这里,我们可以看到大家的删除关键词都是类似的:Delete 和 Drop,不过这里需要注意的是上面术语篇中提过 nGQL 中删除操作对应单词有 Delete 和 Drop ,在 nGQL 中 Delete 一般用于点边,Drop 用于 Schema 删除,这点和 SQL 的设计思路是一样的。

更新数据

复制代码

# nGQLnebula> UPDATE VERTEX hash("jesus") SET character.type = 'titan';# Gremlingremlin> g.V(jesus).property('age', 6000);==>v[32]# Cyphercypher> MATCH (n:character {name:"jesus"}) SET n.type = 'titan';

可以看到 Cypher 和 nGQL 都使用 SET 关键词来设置点对应的类型值,只不过 nGQL 中多了 UPDATE 关键词来标识操作,Gremlin 的操作和查看点操作类似,只不过增加了变更 property 值操作,这里我们注意到的是,Cypher 中常见的一个关键词便是 MATCH,顾名思义,它是一个查询关键词,它会去选择匹配对应条件下的点边,再进行下一步操作。

查看数据

复制代码

# nGQLnebula> FETCH PROP ON character hash("saturn");===================================================| character.name | character.age | character.type |===================================================| saturn         | 10000         | titan          |---------------------------------------------------# Gremlingremlin> g.V(saturn).valueMap();==>[name:[saturn],type:[titan],age:[10000]]# Cyphercypher> MATCH (n:character {name:"saturn"}) RETURN properties(n)  ╒════════════════════════════════════════════╕  │"properties(n)"                             │  ╞════════════════════════════════════════════╡  │{"name":"saturn","type":"titan","age":10000}│  └────────────────────────────────────────────┘

在查看数据这块,Gremlin 通过调取 valueMap() 获得对应的属性值,而 Cypher 正如上面更新数据所说,依旧是 MATCH 关键词来进行对应的匹配查询再通过 RETURN 返回对应的数值,而 nGQL 则对 saturn 进行 hash 运算得到对应 VID 之后去获取对应 VID 的属性值。

查询 hercules 的父亲

复制代码

# nGQLnebula>  LOOKUP ON character WHERE character.name == 'hercules' | \      -> GO FROM $-.VertexID OVER father YIELD $$.character.name;=====================| $$.character.name |=====================| jupiter           |---------------------# Gremlingremlin> g.V().hasLabel('character').has('name','hercules').out('father').values('name');==>jupiter# Cyphercypher> MATCH (src:character{name:"hercules"})-[:father]->(dst:character) RETURN dst.name      ╒══════════╕      │"dst.name"│      ╞══════════╡      │"jupiter" │      └──────────┘

查询父亲,其实是一个查询关系 / 边的操作,这里不做赘述,上面插入边的时候简单介绍了 Gremlin、Cypher、nGQL 这三种图数据库是各自用来标识边的关键词和操作符是什么。

查询 hercules 的祖父

复制代码

# nGQLnebula> LOOKUP ON character WHERE character.name == 'hercules' | \     -> GO 2 STEPS FROM $-.VertexID OVER father YIELD $$.character.name;=====================| $$.character.name |=====================| saturn            |---------------------# Gremlingremlin> g.V().hasLabel('character').has('name','hercules').out('father').out('father').values('name');==>saturn# Cyphercypher> MATCH (src:character{name:"hercules"})-[:father*2]->(dst:character) RETURN dst.name      ╒══════════╕      │"dst.name"│      ╞══════════╡      │"saturn"  │      └──────────┘

查询祖父,其实是一个查询对应点的两跳关系,即:父亲的父亲,我们可以看到 Gremlin 使用了两次 out() 来表示为祖父,而 nGQL 这里使用了 (Pipe 管道) 的概念,用于子查询。在两跳关系处理上,上面说到 Gremlin 是用了 2 次 out(),而 Cypher、nGQL 则引入了 step 数的概念,分别对应到查询语句的 GO 2 STEP 和 [:father *2],相对来说 Cypher、nGQL 这样书写更优雅。

查询年龄大于 100 的人物

复制代码

# nGQLnebula> LOOKUP ON character WHERE character.age > 100 YIELD character.name, character.age;=========================================================| VertexID             | character.name | character.age |=========================================================| 6761447489613431910  | pluto          | 4000          |---------------------------------------------------------| -5860788569139907963 | neptune        | 4500          |---------------------------------------------------------| 4863977009196259577  | jupiter        | 5000          |---------------------------------------------------------| -4316810810681305233 | saturn         | 10000         |---------------------------------------------------------# Gremlingremlin> g.V().hasLabel('character').has('age',gt(100)).values('name');==>saturn==>jupiter==>neptune==>pluto# Cyphercypher> MATCH (src:character) WHERE src.age > 100 RETURN src.name      ╒═══════════╕      │"src.name" │      ╞═══════════╡      │  "saturn" │      ├───────────┤      │ "jupiter" │      ├───────────┤      │ "neptune" │      │───────────│      │  "pluto"  │      └───────────┘

这个是一个典型的查询语句,找寻符合特定条件的点并返回结果,在 Cypher 和 nGQL 中用 WHRER 进行条件判断,而 Gremlin 延续了它的“编程风”用 gt(100) 表示年大于龄 100 的这个筛选条件,延伸下 Gremlin 中 eq() 则表示等于这个查询条件。

从一起居住的人物中排除 pluto 本人

复制代码

# nGQLnebula>  GO FROM hash("pluto") OVER lives YIELD lives._dst AS place | GO FROM $-.place OVER lives REVERSELY WHERE $$.character.name != "pluto" YIELD $$.character.name AS cohabitants;===============| cohabitants |===============| cerberus    |---------------# Gremlingremlin> g.V(pluto).out('lives').in('lives').where(is(neq(pluto))).values('name');==>cerberus# Cyphercypher> MATCH (src:character{name:"pluto"})-[:lives]->()<-[:lives]-(dst:character) RETURN dst.name      ╒══════════╕      │"dst.name"│      ╞══════════╡      │"cerberus"│      └──────────┘

这是一个沿指定点 Pluto 反向查询指定边(居住)的操作,在反向查询中,Gremlin 使用了 in 来表示反向关系,而 Cypher 则更直观的将指向箭头反向变成 <- 来表示反向关系,nGQL 则用关键词 REVERSELY 来标识反向关系。

Pluto 的兄弟们居住在哪

复制代码

# which brother lives in which place?## nGQLnebula> GO FROM hash("pluto") OVER brother YIELD brother._dst AS god | \GO FROM $-.god OVER lives YIELD $^.character.name AS Brother, $$.location.name AS Habitations;=========================| Brother | Habitations |=========================| jupiter | sky         |-------------------------| neptune | sea         |-------------------------## Gremlingremlin> g.V(pluto).out('brother').as('god').out('lives').as('place').select('god','place').by('name');==>[god:jupiter, place:sky]==>[god:neptune, place:sea]## Cyphercypher> MATCH (src:Character{name:"pluto"})-[:brother]->(bro:Character)-[:lives]->(dst)RETURN bro.name, dst.name      ╒═════════════════════════╕      │"bro.name"    │"dst.name"│      ╞═════════════════════════╡      │ "jupiter"    │  "sky"   │      ├─────────────────────────┤      │ "neptune"    │ "sea"    │      └─────────────────────────┘

这是一个通过查询指定点 Pluto 查询指定边 brother 后再查询指定边 live 的查询,相对来说不是很复杂,这里就不做解释说明了。

最后,本文只是对 Gremlin、Cypher、nGQL 等 3 个图查询语言进行了简单的介绍,更复杂的语法将在本系列的后续文章中继续,欢迎在论坛留言交流。