grist支持mysql存储经验总结

        grist是一款支持excel导入,支持数据处理、数据分析及可视化工具,主体使用node ts语言实现,功能与excel类似,差别在于数据存储在服务端,数据处理通过沙箱中的python实现,保证了数据处理的安全。数据存储在sqlite3中,其中sqlite3的api接口是通过C++实现的,是个定制化的api接口,支持数据的DDL、CURD、事务及Backup功能。

        本次的目标是希望能将其数据库存储修改为mysql8,这个修改不仅仅是换个数据库连接那么简单,而是要做很多细节的修改和调整。

        需要充分了解sqlite3和mysql8的差异,如:SQL语句表达上存在差异,主要表现在:

        1.数据类型的差异,需要修改表创建语句,即DDL语句,主要体现在部分Text需要修改为Varchar(256),主要支持

        2.需要修改CURD语句;

        3.需要根据sqlite3的接口,实现mysql的接口,包括:

        a.需要选择支持promise的mysql接口,通过对比mysql,mysql2和mysqlx,最后确定使用mysql2/promise

        b.需要对查询出来的数据进行marshall,因为sqlite3的数据在C++接口中默认是marshall过的,这样数据在grist中才能正常显示,即将数据marshall成Buffer类型。

        c.需要支持事务,事务不支持通过sql命令直接实现,而是使用的connection的事务方法;

        d.查询出来的数据需要转化为sqlite3类似的数据格式,以便直接获取数据字段的值,如:ResultRow类型;

        e.数据插入方面,为了提升性能,需要使用数组的方式进行批量插入,性能可以达到2万+条/秒;

         f.数据更新方面,为了提升性能,需要使用批量更新的方式进行更新,性能可以达到10万+条/秒;

         g.SQL语句转换上,需要注意去掉表名、字段名的双引号"、group等关键字的转换,Json类数据"{}",需要将双引号修改为单引号'{}',进行存储;

         h.数据备份不能使用sqldump工具,只能通过sql按表进行实现(效率比sqldump低)。

    附上部分核心代码供参考(包括事务、批量插入、批量更新、单条查询、批量查询、备份、批量marsall):

public async getConnection(doc_name:string):Promise<mysql.Connection>
  {
    const host = process.env.MYSQL_HOST?process.env.MYSQL_HOST:'localhost'
    const port = process.env.MYSQL_PORT?process.env.MYSQL_PORT:'3310'
    const user = process.env.MYSQL_USER?process.env.MYSQL_USER:'root'
    const password = process.env.MYSQL_PASSWORD?process.env.MYSQL_PASSWORD:'123456789'
    const database = process.env.MYSQL_DATABASE?process.env.MYSQL_DATABASE:'mysql'
    var conn = mysql.createConnection(`mysql://${user}:${password}@${host}:${port}/${database}`);
    (await conn).execute("create database if not exists `"+doc_name+"`");
    (await conn).end();
    return mysql.createConnection(`mysql://${user}:${password}@${host}:${port}/${doc_name}?multipleStatements=true`);
  }
public async backup(src:string, dest:string):Promise<void>{
      const dest_name =  replaceAll(src+'-',"",dest);
      console.log('backup      src:',src);
      console.log('backup      dest:',dest);
      console.log('backup:dest_name:',dest_name);
      const dest_db =  MySQLDB.openDBRaw(dest);
      //获取所有表
      const tblRows = await this.all(`show tables from ${this.doc_name}`)
      for (const tblRow of tblRows) {
        for (const key in tblRow){
          const createTblRows =  await this.all(`show create table ${tblRow[key]}`)
          for (const createTbl of createTblRows){
            for (const key2 in createTbl){
              if (createTbl[key2]!=tblRow[key])
              {
                const create_sql = createTbl[key2];
                await (await dest_db).exec(create_sql);
                const sql = `insert into \`${dest_name}\`.${tblRow[key]} select * from ${tblRow[key]}`;
                await this.exec(sql);
              }
            }
          }
        }
      }
      return
    }
public async run(sql: string, ...params: any[]): Promise<mysql.OkPacket[]> {
    const db = await this.getConnection(this.doc_name)
    sql = replaceAll("\"group\"","`group`",sql);
    sql = replaceAll(" group ","`group`",sql);
    sql = replaceAll("group=","`group`=",sql);  //mysql 中group为关键字
    sql = replaceAll("1e999","0",sql);
    console.log('run sql:',sql, 'params:',params);
    var rows:mysql.OkPacket[]
    if (params.length>0)
      if (typeof params[0]==='object')
        [rows,,] =await db.query(sql,params[0]);
      else
        [rows,,] =await db.query(sql,params);

    else
      [rows,,] =await db.query(sql);
    //console.log('rows:',rows)
    //console.log('fields:',fields)
    db.destroy()
    return rows;
  }
public async get(sql:string, ...params: any[]):Promise<ResultRow> {
      const db = await this.getConnection(this.doc_name)
        console.log('get sql:',sql, 'params:',params);
        var rows:mysql.RowDataPacket[]
        if (params.length>0)
        {
          [rows,,] = (await db.query(sql,params));
        }
        else{
          [rows,,] = (await db.query(sql));
        }
        //console.log('rows:',rows)
        //console.log('fields:',fields)
        var row:ResultRow = {};
        for(var line of rows)
        {
          row = line;
        }
        // console.log('get row:',row);
        db.destroy()
        return row;
    }
public async all(sql: string, ...params: any[]): Promise<ResultRow[]> {
      const db = await this.getConnection(this.doc_name)
        //console.log('all sql:',sql, 'params:',params);
        var rows:mysql.RowDataPacket[]
        if (params.length>0)
        {
          [rows, ,] = (await db.query(sql,params));
        }
        else{
          [rows, ,]  = (await db.query(sql));
        }
        //console.log('fields:',fields)
        var table:ResultRow[]=[];
        for (var line of rows)
        {
            var row:ResultRow ={}
            row = line;
            table.push(row);
        }
        //console.log('all table:',table)
        db.destroy()
        return table;
    }
public async allMarshal(sql: string, ...params: any[]): Promise<Buffer> {
      const db = await this.getConnection(this.doc_name)
      console.log('allMarshal sql:',sql, 'params:',params);
      var rows:mysql.RowDataPacket[], fields:mysql.FieldPacket[]
      if (params.length>0&&params[0].length>0)
      {
        [rows, fields] = (await db.query(sql, params));
      }
      else{
        [rows, fields] = (await db.query(sql));
      }
      //console.log('allMarshal fields:',fields)
      const marshaller = new marshal.Marshaller({version: 2});
      var table:{[key:string]:any[]} = {}
      if (rows.length>0)
      {
        for(var line of rows)
        {
            for(var col of fields)
            {
              var col_value = line[col.name];
              var values = table[col.name];
              if (values === undefined)
                values=[]
              if (Buffer.isBuffer(col_value))//解决BLOB的解码问题
                if (isNumber(col_value.toString()))
                  values.push(Number(col_value.toString()))
                else
                  values.push(col_value.toString())
              else
                values.push(col_value);
              table[col.name]= values;
            }
        }
      }
      else
      {
        for(var col of fields)
          {
            table[col.name]= [];
          }
      }
      marshaller.marshal(table);
      const buf = marshaller.dump();
      db.destroy()
      return Buffer.from(buf);
  }
public async prepare(sql: string, ...params: any[]): Promise<any> {
    const db = await this.getConnection(this.doc_name)
    sql = replaceAll("\"group\"","`group`",sql);
    sql = replaceAll(" group ","`group`",sql);
    sql = replaceAll("group=","`group`=",sql);  //mysql 中group为关键字
    sql = replaceAll("1e999","0",sql);
    console.log('prepare sql:',sql, 'params[0].length:',params[0].length);
    var start = process.uptime();
    var end = process.uptime();
    var begin = process.uptime();
    var diff = 1;
    if (params.length>0)
        //批量插入"INSERT INTO TABLE() VALUES ?",其中?是个包含数组的数组[params[0]]
        if (/INSERT INTO/.test(sql))
        {
          await db.query(sql,[params[0]]);
        }
        else//批量更新
        {
          if (typeof params[0]==='object')
          {
            //sql likes "update table field=? where id in ?"
            //toparams likes [`case id when 1 then "A" end`, [[1,2,3]]]
            const UPDATE_NUM = 200;
            var field_cases:{[key:string]:string}={}
            var id_ins = []
            var i = 0
            var left = 0
            var toparams =[]
            for(var param of params[0])
            {
              var fields:{[key:string]:string|number|boolean} = param[0];
              var ids:{[key:string]:number} = param[1];
              id_ins.push(ids['id']);
              for (var key in fields)
              {
                if (field_cases[key] === undefined)
                {
                  if (typeof fields[key] ==='string')
                    field_cases[key] = `case id when ${ids['id']} then '${fields[key]}' `
                  else
                    field_cases[key] = `case id when ${ids['id']} then ${fields[key]} `
                }
                else
                {
                  if (typeof fields[key] ==='string')
                    field_cases[key] = field_cases[key].concat(`when ${ids['id']} then '${fields[key]}' `)
                  else
                    field_cases[key] = field_cases[key].concat(`when ${ids['id']} then ${fields[key]} `)
                }
              }
              i = i + 1;
              left = i%UPDATE_NUM;
              if (left===0)
              {
                for (var key in field_cases)
                {
                  toparams.push(field_cases[key].concat(' end'));
                }
                toparams.push([id_ins]);
                sql = mysql.format(sql, toparams);
                sql = replaceAll("\'case","case",sql);
                sql = replaceAll("end\'","end",sql);
                sql = replaceAll("\\","",sql);
                //console.log('sql:',sql);
                await db.query(sql);
                end = process.uptime()
                diff = end-start
                //console.log('提交 i:',i, '执行速度:', Math.round(UPDATE_NUM/diff),'/秒');
                start = process.uptime();
                field_cases = {}
                toparams = []
                id_ins = []
              }
            }
            //剩下的
            if (left>0)
            {
              for (var key in field_cases)
              {
                toparams.push(field_cases[key].concat(' end'));
              }
              toparams.push([id_ins]);
              sql = mysql.format(sql, toparams);
              sql = replaceAll("\'case","case",sql);
              sql = replaceAll("end\'","end",sql);
              sql = replaceAll("\\","",sql);
              //console.log('sql:',sql);
              await db.query(sql);
              end = process.uptime()
              diff = end-start
              //console.log('提交 i:',i, '执行速度:', Math.round(left/diff),'/秒');
            }
          }
          else
          {
            console.log('params:',params)
            await db.execute(sql,params);
          }
        }
    else
      await db.query(sql);
    end = process.uptime()
    diff = end-begin
    console.log('prepare sql:', params[0].length, '执行速度:',  Math.round(params[0].length/diff),'/秒');
    db.destroy()
    return
  }
private async _execTransactionImpl<T>(callback: () => Promise<T>): Promise<T> {
    // We need to swallow errors, so that one failed transaction doesn't cause the next one to fail.
    await this._prevTransaction.catch(noop);
    const db = await this.getConnection(this.doc_name)
    await db.beginTransaction()
    try {
      const value = await callback();
      await db.commit()
      return value;
    } catch (err) {
      try {
        await db.rollback()
      } catch (rollbackErr) {
        log.error("MySQLDB[%s]: Rollback failed: %s", this._dbPath, rollbackErr);
      }
      db.destroy()
      throw err;    // Throw the original error from the transaction.
    }
  }

猜你喜欢

转载自blog.csdn.net/wxl781227/article/details/126642747