关于javascript:vivo悟空活动中台打造-Nodejs-版本的MyBatis

经典的架构设计能够逾越工夫和语言,得以传承。

—— 题记

一、背景

悟空流动中台技术文章系列又和大家见面了,天气渐冷,留神保暖。

在往期的系列技术文章中咱们次要集中分享了前端技术的方方面面,如微组件的状态治理,微组件的跨平台摸索,以及有损布局,性能优化等等。还未关注到的同学,如果感兴趣能够查看往期文章。

明天的技术主题要有点不一样,让咱们一起来聊聊悟空流动中台在应用服务层的一些技术建设。

在悟空流动中台的技术架构设计中,咱们充沛拥抱 JavaScript 生态,心愿推动 JavaScript 的全栈开发流程,所以在应用层的服务端,咱们抉择了 Node 作为 BFF(Backend For Fronted) 层解决方案。

心愿借此来充分发挥JavaScript 的效力,能够更高效、更高质量的实现产品的迭代。

通过实际发现 JavaScript 全栈的开发流程给咱们带来的益处:

  1. 前后端应用 JavaScript 来构建,使得前后端更加交融且高效。前后端代码的复用中局部模块和组件不仅仅能够在前端应用,也能够在后端应用。
  2. 缩小了大量的沟通老本。围绕着产品的需要设计和迭代,前端工程师在前端和后端的开发上无缝的切换,保障业务的疾速落地。
  3. 开发者全局开发的视角。让前端的工程师有机会从产品、前端、后端的视角去思考问题和技术的翻新。

当然 Node 只是服务利用开发的一部分。当咱们须要存储业务数据时,咱们还须要一个数据的长久化解决方案。悟空流动中台抉择成熟又牢靠的 MySQL 来作为咱们的数据存储数据库。那咱们就须要思考 Node 和 MySQL 如何搭配能力更好的开释彼此的能力,接下来让咱们一起走上摸索之路。

二、Node 数据长久层现状与思考

1、纯正的 MySQL 驱动

Node-MySQL是 Node 连贯 MySQL的驱动,应用纯 JavaScript 开发,防止了底层的模块编译。让咱们看看如何应用它,首先咱们须要装置这个模块。

示例如下:

npm install mysql # 之前0.9的版本须要这样装置 npm install mysqljs/mysql

惯例应用过程如下:

var mysql      = require('mysql');
var connection = mysql.createConnection({
  host     : '..', // db host
  user     : '..', // db user
  password : '',   // db password
  database : '..'  // which database
});

connection.connect();

connection.query(
  'SELECT id, name, rank FROM lanaguges', 
  function (error, results, fields) {
    if (error) throw error;
    /**
     * 输入:
     * [ RowDataPacket {
     *    id: 1,
     *    name: "Java",
     *    rank: 1
     *  },
     *  RowDataPacket {
     *    id: 2,
     *    name: "C",
     *    rank: 2
     *  }
     *]
     *
     *
     */
    console.log('The language rank is: ', results);
});

connection.end();

通过上述的例子,咱们对 MySQL 模块的应用形式有个简略的理解,根本的应用形式就是创立连贯,执行 SQL 语句,失去后果,敞开连贯等。

在理论的我的项目中咱们很少间接应用该模块,个别都会在该模块的根底上进行封装,如:

  • 默认应用数据库连接池的形式来晋升性能。
  • 改良callback的回调函数的格调,迁徙到 promise,async/await 更现代化 JavaScript 的异步解决计划。
  • 应用更加灵便的事务的解决。
  • 针对简单 SQL 的编写,通过字符串拼接的形式是比拟苦楚的,须要更语义化的 SQL 编写能力。

2、支流的 ORM

目前在数据长久层技术解决方案中 ORM 依然支流的技术计划,ORM是”对象-关系映射”(Object/Relational Mapping)的缩写,简略来说ORM 就是通过实例对象的语法,实现关系型数据库的操作的技术,如图-1。

无论是 Java 的 JPA 技术规范以及 Hibernate 等技术实现,或者 Ruby On Rails 的 ActiveRecord,亦或 Django 的 ORM。简直每个语言的生态中都有本人的ORM 的技术实现计划。

图-1 O/R Mapping

ORM 把数据库映射成对象:

  • 数据库的表(table) => 类(class)
  • 记录(row,行数据)=> 对象(object)
  • 字段(field)=> 对象的属性(attribute)

Node 在 ORM 的技术计划上,社区有不同的角度的摸索,充分体现了社区的多样性,比方目前十分风行的 Sequelize。Sequelize 是一个基于 Promise 的 Node.js ORM, 目前反对 PostgreSQL、MySQL、SQLite 以及 SQL-Server。它具备弱小的事务反对、关联关系、预读、提早加载、读取复制等性能。如上述 MySQL 应用的案例,若应用Sequelize ORM形式来实现,代码如下:

// 定义ORM的数据与model映射
const Language = sequelize.define('language', {
  // 定义id, int型 && 主键
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true
  },
  // 定义name, string类型映射数据库varchar
  name: {
    type: DataTypes.STRING,
  },
  // 定义rank, string类型映射数据库varchar
  range: {
    type: DataTypes.INTEGER
  },
}, {
  // 不生成工夫戳
  timestamps: false
});

// 查问所有
const languages = await Language.findAll()

3、将来之星 TypeORM

自从有了 TypeScript 之后,让咱们从另外一个视角去对待前端的工具链和生态,TypeScript 的类型体系给了咱们更多的设想,代码的动态查看纠错、重构、主动提醒等。带着这些新视角呈现了社区比拟热捧的 TypeORM。也十分值得咱们借鉴学习。

图-2 TypeORM

TypeORM 充沛联合 TypeScript,提供更好的开发体验。其指标是始终反对最新的 JavaScript 性能,并提供其余性能来帮忙您开发应用数据库的任何类型的应用程序,从带有大量表的小型应用程序到具备多个数据库的大型企业应用程序。

与现有的所有其余 JavaScript ORM 不同,TypeORM 反对 Active Record (RubyOnRails 的 ORM 的外围)和 Data Mapper (Django 的 ORM 的外围设计模式)模式,这意味着咱们能够以最无效的形式编写高质量、涣散耦合、可伸缩、可保护的应用程序。

4、感性思考

在家喻户晓软件开发中,并不存在真正的银弹计划,ORM 给咱们带来了更快的迭代速度,也还是存在一些有余。体现在:

  • 对于简略的场景 CRUD 十分快,对于多表和简单关联查问就会有点力不从心。
  • ORM 库不是轻量级工具,须要花很多精力学习和设置。
  • 对于简单的查问,ORM 要么是无奈表白,要么是性能不如原生的 SQL。
  • ORM 形象掉了数据库层,开发者无奈理解底层的数据库操作,也无奈定制一些非凡的 SQL。
  • 容易产生N+1查问的问题。

咱们开始思考怎么在 ORM 的根底上,保留强悍的 SQL 的表达能力呢?最终,咱们把眼光停留在了 Java 社区十分风行的一款半自动化的 ORM 的框架下面 MyBatis。

三、悟空流动中台在数据长久层的摸索

通过思考,咱们回归原点从新扫视这个问题,咱们认为 SQL 是程序和数据库交互最好的畛域语言,简略易学通用性强且无需回避 SQL 自身。同时 MyBatis 的架构设计给与咱们启发,在技术上是能够做到保留 SQL 的灵便弱小,同时兼顾从 SQL 到对象的灵便映射。

1、什么是 MyBatis ?

MyBatis 是一款优良的长久层框架,它反对自定义 SQL、存储过程以及高级映射。MyBatis 罢黜了简直所有的 JDBC 代码以及设置参数和获取后果集的工作。MyBatis 能够通过简略的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,一般老式 Java 对象)为数据库中的记录。MyBatis 的最棒的设计就是在对象的映射和原生 SQL 弱小之间获得了很好的均衡。

SQL 配置

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select  name  from blog where id = #{id}
  </select>

SQL 查问

BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);

于是咱们开始构建 Node 的 MyBatis,技术上实现的 Node-MyBatis 具备的个性

  • 简略易学。代码实现小而简略。没有任何第三方依赖,易于应用。
  • 灵便。Node-Mybatis 不会对应用程序或者数据库的现有设计强加任何影响。借助 ES6 的 string template编写 SQL,灵便间接。
  • 解除 SQL 与程序代码的耦合。

通过提供 DAO 层,将业务逻辑和数据拜访逻辑拆散,使零碎的设计更清晰,更易保护,更易单元测试。

  • 反对动静 SQL 。防止 SQL 的字符串拼接。
  • 避免 SQL 注入。主动对动静参数进行 SQL 防注入。
  • 申明式事务机制。借助 decorator 更容易进行事务申明。
  • 联合 Typescript 的类型。依据数据的表格构造主动生成数据的类型定义文件,代码晋升补齐,晋升开发体验。

2、Node-MyBatis 解决方案

在咱们业务开发中,咱们构建的 SQL 必定须要依据业务进行判断和动静拼接,如果每条 SQL 都本人手动的拼接又回到了 MySQL 奢侈的模式,一不小心就造成了大量的 SQL 注入等问题,那咱们怎么办呢?这个时候就须要召唤出 Node-MyBatis 的动静 SQL 的 uilder 模式了。

(1)SQL-Builder

\# 表达式

:针对动静 SQL中的占位符,咱们最常常碰到的场景就是字符串的占位符,# 前面就是未来动静替换的变量的名称。如:

SELECT
    id as id,
    book_name as bookName
    publish_time as publishTime
    price as price
  FROM t_books t
  WHERE
    t.id = #data.id AND t.book_name = #data.bookName
    
 -- 该 SQL 通过 Node-MyBatis 底层的 SQL Compile 解析之后,生成的 SQL如下,
 -- data 参数为: {id: '11236562', bookName: 'JavaScript红皮书' }
 
 SELECT
    id as id,
    book_name as bookName
    publish_time as publishTime
    price as price
  FROM t_books t
  WHERE
    t.id = '11236562' AND t.book_name = 'JavaScript红皮书'

$ 表达式

$: 动态数据的占位符,该占位符会在咱们的 sql template 编译后将变量的值动静插入 SQL ,如下:

SELECT 
  id, name, email 
FROM t_user t
WHERE t.state=$data.state AND t.type in ($data.types)

-- 该 SQL 通过 Node-MyBatis 底层的 SQL Compile 解析之后,生成的 SQL如下
-- data 参数为: {state: 1, types: [1,2,3]}

SELECT 
  id, name, email 
FROM t_user t
WHERE t.state=0 AND t.type in (1,2,3)

<%%> 代码块

模板也是语言,那就是图灵齐备的,循环、分支构造都是必不可少的。咱们须要提供动静的编程的能力来应答更加简单的 SQL 场景,那如何进行代码块的标记呢?悟空采纳相似 EJS 模板的语法特色 <%%> 进行代码标记,并且来升高了 SQL 模版学习的难度。上面演示在 SQL 模板中的应用办法。

-- 循环
SELECT
    t1.plugin_id as pluginId,
    t1.en_name  as pluginEnName,
    t1.version as version,
    t1.state as state
 FROM test_npm_list  t1
    WHERE t1.state = '0'
  <% for (let [name, version] of data.list ) { %>
     AND t1.en_name = #name AND t1.version=#version
  <% } %>

-- 分支判断
SELECT 
   id,
   name, 
   age 
FROM users
WHERE name like #data.name
<% if(data.age > 10) {%>
AND age = $data.age
<% } %>

那如何实现上述的性能呢?

咱们通过借助 ES6 的 String Template 能够实现一个十分精简的模板零碎。上面咱们来通过模板字符串输入模板后果的案例。

let template = `
<ul>
  <% for(let i=0; i < data.users.length; i++) { %>
    <li><%= data.users[i] %></li>
  <% } %>
</ul>
`;

下面代码在模板字符串之中,搁置了一个惯例模板。该模板应用   <%…%\> 搁置 JavaScript 代码,应用 <%= … %> 输入 JavaScript 表达式。怎么编译这个模板字符串呢?思路是将其转换为 JavaScript 表达式字符串,指标就是转化为下述字符串。

print('<ul>');
for(let i = 0; i < data.users.length; i++) {
  print('<li>');
  print(data.users[i]);
  print('</li>');
};
print('</ul>');

第一:采纳了正则表达式进行匹配转化。

let evalExpr = /<%=(.+?)%>/g;
let expr = /<%([\s\S]+?)%>/g;

template = template
  .replace(evalExpr, '`); \n  print( $1 ); \n  echo(`')
  .replace(expr, '`); \n $1 \n  print(`');

template = 'print(`' + template + '`);';
console.log(template);

// 输入

echo(`
<ul>
  `);
  for(let i=0; i < data.supplies.length; i++) {
  echo(`
    <li>`);
  echo(  data.supplies[i]  );
  echo(`</li>
  `);
  }
  echo(`
</ul>
`);

第二:将 template 正则封装在一个函数外面返回。这样就实现了模板编译的能力,残缺代码如下:

function compile(template){
  const evalExpr = /<%=(.+?)%>/g;
  const expr = /<%([\s\S]+?)%>/g;

  template = template
    .replace(evalExpr, '`); \n  print( $1 ); \n  echo(`')
    .replace(expr, '`); \n $1 \n  print(`');

  template = 'print(`' + template + '`);';

  let script =
  `(function parse(data){
    let output = "";

    function print(html){
      output += html;
    }

    ${ template }

    return output;
  })`;

  return script;
}

第三:通过 compile 函数,咱们获取到了一个 SQL Builder的 高阶函数,传递参数,即可获取最终的 SQL 模板字符串。

let parse = eval(compile(template));
parse({ users: [ "Green", "John", "Lee" ] });
//   <ul>
//     <li>Green</li>
//     <li>John</li>
//     <li>Lee</li>
//   </ul>

依据这种模板的思路,咱们设计本人的 sqlCompile 来生成 SQL 的代码。

sqlCompile(template) {
    template =
        'print(`' +
        template
            // 解析#动静表达式
            .replace(/#([\w\.]{0,})(\W)/g, '`); \n  print_str( $1 ); \n  print(`$2')
            // 解析$动静表达式
            .replace(/\$([\w\.]{0,})(\W)/g, '`); \n  print( $1 ); \n  print(`$2')
            // 解析<%%>动静语句
            .replace(/<%([\s\S]+?)%>/g, '`); \n $1 \n  print(`') +
        '`);'
    return `(function parse(data,connection){
      let output = "";
      function print(str){
        output += str;
      }
      function print_str(str){
       output += "\'" + str + "\'";
      }
      ${template}
      return output.replace(/[\\r\\n]/g,"");
    })`
  }

(2)SQL 防注入

SQL 反对拼接就可能存在 SQL 的注入可能性,Java 中 MyBatis  $ 动静表达式的应用也是有注入危险的,因为 $ 能够置换变量不会被包裹字符引号,社区也不倡议应用 $ 符号来拼接 SQL。对于 Node-MyBatis 来说,因为保留了 $ 的能力,所以须要解决 SQL 注入的危险。参考 MyBatis 的 Node-MyBatis 工具用法也比较简单,示例如下:

// data = {name: 1}
`db.name = #data.name` // => 字符替换,会被本义成  db.name = "1"
`db.name = $data.name` // => 残缺替换,会被本义成  db.name =  1

注入场景

// SQL 模板
`SELECT * from t_user WHERE username = $data.name and paasword = $data.passwd`
// data 数据为 {username: "'admin' or 1 = 1 --'", passwd: ""}
// 这样通过 SQL正文结构 造成了SQL的注入
`SELECT * FROM members WHERE username = 'admin' or 1 = 1 -- AND password = ''`

// SQL 模板
`SELECT * from $data.table`
// data 数据为 {table: "user;drop table user"}
// 这样通过 SQL正文结构 造成了SQL的注入
`SELECT * from user;drop table user`

针对常见的拼接 SQL 的场景,咱们就不一一叙述了。上面将从常见的不可避免的拼接常见动手,和大家解说 Node-Mybatis 的躲避计划。该计划应用 MySQL 内置的 escape 办法或 SQL 关键字拦挡办法进行参数传值躲避。

escape本义,应用 $ 的进行传值,模板底层会先走 escape 办法进行本义,咱们用一个蕴含不同的数据类型的数据进行 escape 能力检测,如:

const arr = escape([1,"a",true,false,null,undefined,new Date()]);

// 输入
( 1,'a', true, false, NULL, NULL, '2019-12-13 16:19:17.947')

关键字拦挡,在 SQL 须要应用到数据库关键字,如表名、列名和函数关键字 where、 sum、count 、max 、 order by 、 group by 等。若间接拼装 SQL 语句会有比拟显著的 SQL 注入隐患。因而要束缚 $ 的符号的应用值范畴。非凡业务场景,如动静排序、动静查问、动静分组、动静条件判断等,须要开发人员前置枚举判断可能呈现的确定值再传入SQL。Node-MyBatis 中默认拦挡了高风险的 $ 入参关键字。

if(tag === '$'){
  if(/where|select|sleep|benchmark/gi.test(str)){
    throw new Error('$ value not allowed include where、select、sleep、benchmark keyword !')
  }
  //...
}

配置拦挡,咱们为了管制 SQL 的注入危险,在 SQL 查问时默认不反对多条语句的执行。MySQL 底层驱动也有雷同的选项,默认敞开。在 MySQL 驱动的文档中提供了具体的解释如下:

Connection options – 连贯属性

multipleStatements: 

  • Allow multiple mysql statements per query. Be careful with this, it could increase the scope of SQL injection attacks. (Default: false)
  • 每个查问容许多个mysql语句。 请留神这一点,它可能会减少SQL注入攻打的范畴。 (默认值:false)

Node-MyBatis 中默认躲避了多行执行语句的配置与 $ 独特应用的场景。

if(tag === '$'){
  if(this.pool.config.connectionConfig.multipleStatements){
    throw new Error('$ and multipleStatements mode not allowed to be used at the same time !')
  }
  //...
}

SQL 注入检测

sqlmap 是一个开源的浸透测试工具,能够用来进行自动化检测,利用 SQL 注入破绽,获取数据库服务器的权限。它具备功能强大的检测引擎,针对各种不同类型数据库的浸透测试的性能选项,包含获取数据库中存储的数据,拜访操作系统文件甚至能够通过外带数据连贯的形式执行操作系统命令。sqlmap 反对 MySQL, Oracle, PostgreSQL, Microsoft SQL Server, Microsoft Access, IBM DB2, SQLite, Firebird, Sybase 和 SAP MaxDB 等数据库的各种安全漏洞检测。

sqlmap 反对五种不同的注入模式:

  • 基于布尔的盲注 即能够依据返回页面判断条件虚实的注入;
  • 基于工夫的盲注 即不能依据页面返回内容判断任何信息,用条件语句查看时间延迟语句是否执行(即页面返回工夫是否减少)来判断;
  • 基于报错注入 即页面会返回错误信息,或者把注入的语句的后果间接返回在页面中;
  • 联结查问注入 能够应用union的状况下的注入;
  •  堆查问注入能够同时执行多条语句的执行时的注入。

图-3 – SQLmap的应用

装置&应用

//装置办法
git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev

//应用办法
sqlmap -u 'some url' --flush-session --batch --cookie="some cookie"

常用命令参数

  • -u 设置想要验证的网站url
  • –flush-session 革除过来的历史记录
  • –batch 批量验证注入
  • –cookie如果须要登录 设置cookie值

明确 sqlmap  应用办法后,咱们在理论我的项目打包过程中能够基于 sqlmap 构建咱们的自定义化测试脚本,在提交代码之后,通过 GitLab 的集成工具主动触发进行工程的验证。

(3)申明式事务

在 Node 和数据库的交互上,针对更新的 SQL 场景,咱们须要对事务进行治理,手动治理事务比拟费时费力,Node-MyBatis 提供了更好的事务管理机制,提供了申明式的事务管理能力,将咱们从简单的事务处理中解脱进去,获取连贯、敞开连贯、事务提交、回滚、异样解决等这些操作都将主动解决。

申明式事务管理应用了 AOP 实现的,实质就是在指标办法执行前后进行拦挡。在指标办法执行前退出或创立一个事务,在执行办法执行后,依据理论状况抉择提交或是回滚事务。不须要在业务逻辑代码中编写事务相干代码,只须要在配置文件配置或应用注解(@Transaction),这种形式没有侵入性。

在代码的实现上,咱们应用 ES7 标准中装璜器的标准,来实现对指标类,办法,属性的润饰。装璜器的应用非常简单,其本质上就是一个函数包装。上面咱们封装一个简略的 log 装璜器函数。

装璜类

function log(target, name, descriptor) {
  console.log(target)
  console.log(name)
  console.log(descriptor)
}

@log
class User {
  walk() {
    console.log('I am walking')
  }
}

const u = new User()
u.walk()

装璜办法

function log(target, name, descriptor) {
  console.log(target)
  console.log(name)
  console.log(descriptor)
}

class Test {
  @log // 装璜类办法的装璜器
  run() {
    console.log('hello world')
  }
}

const t = new Test()
t.run()

装璜器函数有三个参数,其含意在装璜不同属性时体现也不必。在装璜类的时候,第一个参数示意类的函数自身。之前 log 输入如下:

[Function: User]
undefined
undefined
I am walking

在装璜类办法的时候,第一个参数示意类的原型( prototype ), 第二个参数示意办法名, 第三个参数示意被装璜参数的属性。之前 log 输入如下:

Test { run: [Function] }
run
{
  value: [Function],
  writable: true,
  enumerable: true,
  configurable: true
}
hello world

第三个 describe 参数内有如下属性:

  1. configurable – 管制是不是能删、能批改descriptor自身。
  2. writable – 管制是不是能批改值。
  3. enumerable – 管制是不是能枚举出属性。
  4. value – 管制对应的值,办法只是一个value是函数的属性。
  5. get和set – 管制拜访的读和写逻辑。

实现机制

对于 ES7 的装璜器一些弱小的个性和用法能够参考 TC39 的提案,这里就不累述。来看看咱们 @Transaction 的实现:

// 封装高阶函数
function Transaction() {
  // 返回代理办法
  return (target, propetyKey, descriptor) => {
    // 获取以后的代理办法
    let original = descriptor.value;
    // 拦挡扩大办法
    descriptor.value = async function (...args) {
     try {
       // 获取底层mysql的事务作用域
        await this.ctx.app.mysql.beginTransactionScope(async (conn) => {
          // 绑定数据库连贯
          this.ctx.conn = conn
          // 执行办法
          await original.apply(this, [...args]);
      }, this.ctx);
     } catch (error) {
       // 错误处理...
      this.ctx.body = {error}
     }
    };
  };
}

在 Transaction 的装璜器中,咱们应用底层 egg-mysql 对象扩大的 beginTransactionScope 自动控制,带作用域的事务 。

API:beginTransactionScope(scope, ctx)

  • scope:  一个 generatorFunction,它将执行此事务的所有SQL。
  • ctx:  以后申请的上下文对象,它将确保即便在嵌套的状况下事务,一个申请中同时只有一个流动事务。 
const result = yield app.mysql.beginTransactionScope(function* (conn) {
  // 不须要手动处理事务开启和回滚
  yield conn.insert(table, row1);
  yield conn.update(table, row2);
  return { success: true };
}, ctx); // ctx 执行上下文

联合 Midway 的应用

import { Context, controller, get, inject, provide } from "midway";

@provide()
@controller("/api/user")
export class UserController {

  @get("/destroy")
  @Transaction()
  async destroy(): Promise<void> {
    const { id } = this.ctx.query;
    const user = await this.ctx.service.user.deleteUserById({ id });
    // 如果产生失败,上述的数据库操作主动回滚
    const user2 = await this.ctx.service.user.deleteUserById2({ id });
    this.ctx.body = { success: true, data: [user,user2] };
  }
}

(4)更多个性迭代中

数据缓存,为了晋升数据的查问的效率,咱们正在迭代开发 Node-MyBatis 的缓存机制,缩小对数据库数据查问的压力,晋升整体数据查问的效率。

MyBatis 提供了一级缓存,和二级缓存,咱们在架构上也进行了参考,架构图如下图-4。

一级缓存是 SqlSession 级别的缓存,在同一个 SqlSession 中两次执行雷同的 SQL 语句,第一次执行结束会将数据库中查问的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查问,从而进步查问效率。当一个 SqlSession 完结后该 SqlSession 中的一级缓存也就不存在了。不同的 SqlSession 之间的缓存数据区域是相互不影响。

二级缓存是 Mapper 级别的缓存,多个 SqlSession 去操作同一个 Mapper 的 SQL 语句失去数据会存在二级缓存区域,多个 SqlSession 能够共用二级缓存,二级缓存是跨SqlSession的。

图-4 缓存架构图

自定义办法和标签,在 SQL 模版中,咱们通过 #、$、<%%> 来实现 SQL 的动静构建,不过在我的项目实战中咱们发现很多反复的一些SQL 拼接场景,针对这些场景咱们正在开发在 SQL 模板中反对自定义的办法和标签,间接内置在模板中应用晋升开发效率,并且提供插件机制,不便开发者开发本人的自定义办法和标签。上面咱们看看通过自定义办法和标签对 SQL 构建的一些小例子。

数据插入

目前的数据插入方式,放弃了 native SQL 的形式,然而,当数据库的字段特地多的时候,一个个去列出插入的字段是比拟累的一件事件。特地是当咱们标准强调 SQL 插入时必须指定插入的列名,防止数据插入不统一。

INSERT INTO 
  test_user 
(name, job, email, age, edu)
 VALUES 
  (#data.name, #data.job, #data.email, #data.age, #data.edu)

Node-MyBatis 内置办法 – quickInsert()

-- user = {
--  name: 'test', 
--  job: 'Programmer', 
--  email: 'test@test1.com', 
--  age: 25, 
--  edu: 'Undergraduate'
-- }

-- sql builder
INSERT INTO test_user <% quickInsert(data.user) %>

-- 通过 SQL compiler 主动输入

INSERT INTO 
  test_user (name, job, email, age, edu) 
  VALUES('test', 'Programmer', 'test@test1.com', 25, 'Undergraduate')



-- userList =   [
--  {name: 'test', job: 'Programmer', email: 'test@test1.com',  age: 25, edu: 'Undergraduate'}, 
--  {name: 'test2', job: 'Programmer', email: 'test@test2.com',  age: 30, edu: 'Undergraduate'}
-- ]

-- 批量插入
INSERT INTO test_user <% quickInsert(data.userList)%>

-- 通过 SQL compiler 主动输入

INSERT INTO 
  test_user (name, job, email, age, edu) 
  VALUES 
    ('test', 'Programmer', 'test@test1.com', 25, 'Undergraduate'),
    ('test2', 'Programmer', 'test@test2.com', 30, 'Undergraduate')

Node-MyBatis 内置标签 – <Insert />

-- user = {
--  name: 'test', 
--  job: 'Programmer', 
--  email: 'test@test1.com', 
--  age: 25, 
--  edu: 'Undergraduate'
-- }

-- sql builder
<Insert table="test_user" values={data.user}></Insert>

-- 通过 SQL compiler 主动输入
INSERT INTO 
  test_user (name, job, email, age, edu) 
  VALUES('test', 'Programmer', 'test@test1.com', 25, 'Undergraduate')


-- userList =   [
--  {name: 'test', job: 'Programmer', email: 'test@test1.com',  age: 25, edu: 'Undergraduate'}, 
--  {name: 'test2', job: 'Programmer', email: 'test@test2.com',  age: 30, edu: 'Undergraduate'}
--]

-- sql builder
<Insert table="test_user" values={data.userList}></Insert>

--通过 SQL compiler 主动输入
INSERT INTO 
  test_users (name, job, email, age, edu) 
  VALUES 
    ('test', 'Programmer', 'test@test1.com', 25, 'Undergraduate'),
    ('test2', 'Programmer', 'test@test2.com', 30, 'Undergraduate')

目前 Node-MyBatis 基于 Midway 的插件标准内置在我的项目中,目前正在抽离独立模块造成独立的解决方案,而后针对 Midway 进行适配对接,晋升计划的独立性。

3、Node-MyBatis 实战

(1)API

/**
 * 查问合乎所有的条件的数据库记录
 * @param sql: string sql字符串
 * @param params 传递给sql字符串动静变量的对象
 */
query(sql, params = {})

/**
 * 查问符合条件的数据库一条记录
 * @param sql: string sql字符串
 * @param params 传递给sql字符串动静变量的对象
 */
queryOne(sql, params = {})

/**
 * 插入或更新数据库记录
 * @param sql: string sql字符串
 * @param params 传递给sql字符串动静变量的对象
 */
exec(sql, params = {})

(2)我的项目构造

因为咱们抉择应用 Midway 作为咱们的 BFF 的 Node 框架, 所以咱们的目录构造遵循规范的 Midway 的构造。

.
├── controller                # 入口 controller 层
│   ├── base.ts               # controller 公共基类
│   ├── table.ts
│   └── user.ts
├── extend                    # 对 midway 的扩大
│   ├── codes
│   │   └── index.ts
│   ├── context.ts
│   ├── enums                 # 枚举值
│   │   ├── index.ts
│   │   └── user.ts
│   ├── env                   # 扩大环境
│   │   ├── index.ts
│   │   ├── local.ts
│   │   ├── prev.ts
│   │   ├── prod.ts
│   │   └── test.ts
│   ├── helper.ts             # 工具办法
│   └── nodebatis             # nodebatis 外围代码
│       ├── decorator         # 申明式事务封装
│       ├── plugin            # 自定义工具办法
│       ├── config.ts         # 外围配置项
│       └── index.ts
├── middleware                # 中间件层
│   └── error_handler.ts      # 扩大错误处理
├── public
└── service                  # 业务 service 层
    ├── Mapping              # node-mybatis的 mapping层
    │   ├── TableMapping.ts
    │   └── UserMapping.ts
    ├── table.ts             # table service 和 db相干调用 TableMapping
    └── user.ts              # user service 和 db相干调用 UserMapping

(3)业务场景

依据用户 id 查问用户信息,当 Node 服务收到用户信息的查问申请,依据 URL 的规定路由将申请分派到 UserController 的 getUserById 办法进行解决,getUserById 办法通过对 UserService 进行调用实现相干数据的获取,UserService 通过 Node-MyBatis 实现对数据库用户信息的查问。

Controller层

//controller/UserController.ts
import {  controller, get, provide } from 'midway';
import BaseController from './base'

@provide()
@controller('/api/user')
export class UserController extends BaseController {
  /**
   * 依据用户id查问所有用户信息
   */
  @get('/getUserById')
  async getUserById() {
    const { userId } = this.ctx.query;
    let userInfo:IUser = await this.ctx.service.user.getUserById({userId})
    this.success(userInfo)
  }
}

Service层

// service/UserService.ts
import { provide } from 'midway';
import { Service } from 'egg';
import UserMapping from './mapping/UserMapping';

@provide()
export default class UserService extends Service {
  getUserById(params: {userId: number}): Promise<{id: number, name: string, age: number}> {
     return this.ctx.helper.queryOne(UserMapping.findById, params);
  }
}

DAO层

// service/mapping/UserMapping.ts
export default {
  findById: `
    SELECT 
      id,
      name,
      age
    FROM users t1
    WHERE
      t1.id=#data.userId
  `
}

4、工程化体系

(1)类型体系

在 Node 服务的开发中,咱们须要更多的工程化的能力,如代码的提醒和主动补全、代码的查看、重构等,所以咱们抉择 TypeScript 作为咱们的开发语言,同时 Midway 也提供了很多的对于 Typescript 的撑持。

咱们心愿咱们的 Node-MyBatis 也能插上类型的翅膀,能够依据查问的数据可能主动查看纠错补齐。于是就产生了咱们 tts (table to typescript system) 解决方案, 能够依据数据库的元数据主动生成 TypeScript 的类型定义文件。

图-5 数据库表构造

通过 tts -t test_user 主动生成表单的类型定义文件,如下:

export interface ITestUser {
  /**
   * 用户id
   */
  id: number

  /**
   * 用户名
   */
  name: string

  /**
   * 用户状态
   */
  state: string

  /**
   * 用户邮箱
   */
  email: string

  /**
   * 用户年龄
   */
  age: string
}

这样在开发中就能够应用到这个类型文件,给咱们的日常开发带来一些便当。再联合上TypeScript的高级类型容器如Pick、Partial、Record、Omit等能够依据查问的字段进行简单类型的适配。

图-6 tts 类型文件的应用

(2)LSP

VSCode 根本成为前端开发编辑器的第一抉择,另外通过 VSCode 架构中的 LSP(语言服务协定)能够实现很多 IDE 的性能,为咱们的开发提供更智能的帮忙。

比方咱们在应用 Node-MyBatis 中须要编写大量的 SQL 字符串,对于 VSCode 来说,这就是一个一般的 JavaScript 的字符串,没有任何非凡之处。

然而咱们期待可能走得更远,比方能自动识别 SQL 的关键字,语法高亮,并且实现 SQL 的主动丑化。通过开发 VSCode 插件,对 SQL 的语法特色智能剖析,能够做到如下成果,实现 SQL 代码高亮和格式化,后续还会反对 SQL 的主动补齐。

图-7 SQL 字符串模板的高亮和格式化

另外,因为反对了自定义模板和自定义办法之后,编写 SQL 的效率失去了晋升,对于 SQL 生成就变得不再直观,须要在运行期才晓得 SQL 的字符串内容。怎么解决这个痛点,其实也能够通过 LSP 解决这个问题,做到这效率和可维护性的均衡,通过开发  VSCode 的插件, 智能剖析 SQL 模板构造实时悬浮提醒生成的 SQL 。

四、总结

文章到了这里,曾经进入结尾,感谢您的关注和陪伴。本文咱们一起回顾了悟空流动中台 Node 服务数据层长久化解决方案设计上的一些思考和摸索,咱们心愿保留 SQL 的简略通用弱小,又能保障极致的开发体验,心愿通过 Node-MyBatis 的设计兑现了咱们的思考。

作者: vivo 悟地面台研发团队

往期浏览:

  • 悟空流动中台 \- 栅格布局计划
  • 悟空流动中台 \- 基于 WebP 的图片高性能加载计划
  • 悟空流动中台 \- H5 流动加载优化
  • 悟空流动中台 \- 微组件多端摸索
  • 悟空流动中台 \- 基于行为预设的动静布局计划

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理