Node.js 连接金仓数据库踩坑记(上篇):环境搭建与基础操作

11 阅读5分钟

Node.js 连接金仓数据库踩坑记(上篇):环境搭建与基础操作

开篇:一个让我熬夜到凌晨两点的坑

去年做一个内部管理系统,后端用的是Node.js。客户要求数据库必须用金仓,我当时心里还挺有底的——Node.js生态这么丰富,连个数据库驱动还不简单?

结果一搜,发现金仓的Node.js驱动不是我熟悉的那种npm install就能用的方式。下载、拷贝、版本匹配、回调嵌套......折腾了一整晚才跑通。

这篇文章分上下两篇。上篇讲环境搭建和基础CRUD,下篇讲连接池、事务、预处理语句这些稍微高级点的用法。今天先说说怎么把环境跑起来。

一、驱动配置

1.1 下载驱动包

金仓的Node.js驱动不是通过npm发布的,需要去官网下载。下载下来是一个压缩包,解压后里面有个node_modules文件夹,还有一个测试用例。

我一开始还纳闷:怎么不是.tgz文件?后来才知道,这个驱动需要手动放置,不能直接npm install

1.2 放置驱动

把你项目里的node_modules文件夹和驱动里的node_modules合并一下就行。

项目的目录结构看起来像这样:

my-kingbase-project/
├── node_modules/
│   └── kb/                    # 从驱动包拷贝过来的
│       ├── index.js
│       ├── lib/
│       └── ...
├── app.js
└── package.json

1.3 版本兼容性

这个驱动是基于Node.js 10.19.0开发的。官方文档说支持Node.js 8、10、12。我一开始用的是Node.js 14,结果require('kb')的时候报了一堆莫名其妙的错,换成Node.js 12就好了。

如果你用高版本Node跑不起来,降级试试。这个坑我花了两个小时才爬出来。

建议先用nvm切到Node.js 12,跑通之后再考虑要不要升。

二、连接数据库

2.1 基本连接方式

驱动提供了一个Client类,通过它来连接数据库。这个设计跟pg模块很像,用过的人应该不陌生。

const { Client } = require('kb');

const client = new Client({
    user: 'SYSTEM',
    host: '127.0.0.1',
    database: 'TEST',
    password: '123456',
    port: 54321,
    ssl: false
});

client.connect(err => {
    if (err) {
        console.error('连接失败', err.stack);
    } else {
        console.log('连接成功');
    }
});

参数说明:

参数名说明
user数据库用户名,默认SYSTEM
host数据库IP地址
database要连接的数据库名
password密码
port端口号,默认54321
ssl是否启用SSL,默认false

2.2 异步/等待模式

回调写起来有点繁琐,可以包装成Promise。我用util.promisify处理了一下:

const { Client } = require('kb');
const { promisify } = require('util');

async function connectDB() {
    const client = new Client({
        user: 'SYSTEM',
        host: '127.0.0.1',
        database: 'TEST',
        password: '123456',
        port: 54321
    });
    
    const connect = promisify(client.connect).bind(client);
    await connect();
    
    return client;
}

// 使用
const client = await connectDB();

三、执行SQL语句

3.1 不带参数的查询

连接成功后,可以用query方法执行SQL。这个方法是异步的,需要传入回调函数。

client.query('SELECT * FROM users', (err, res) => {
    if (err) {
        console.error('查询失败', err.stack);
        return;
    }
    console.log(res.rows);
});

返回的res对象里,rows是结果集数组,每行数据就是一个对象,字段名对应键名。

3.2 带参数的查询

参数占位符用$1, $2,不需要手动拼接字符串,能防止SQL注入。

方式一:分开传参

const text = 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *';
const values = ['张三', 'zhangsan@test.com'];

client.query(text, values, (err, res) => {
    if (err) {
        console.log(err.stack);
    } else {
        console.log('插入成功', res.rows[0]);
    }
});

方式二:用对象传参

const query = {
    text: 'INSERT INTO users(name, email) VALUES($1, $2)',
    values: ['李四', 'lisi@test.com']
};

client.query(query, (err, res) => {
    if (err) {
        console.log(err.stack);
    } else {
        console.log('插入成功');
    }
});

RETURNING *很实用,能直接拿到刚插入的数据,省得再查一次。

3.3 预处理语句

如果要多次执行同一条SQL,可以用预处理语句(Prepared Statement)。它会提前解析SQL模板,多次执行时只需要传参数,效率更高。

const query = {
    name: 'insert-user',  // 语句名称
    text: 'INSERT INTO users(name, email) VALUES($1, $2)',
    values: ['王五', 'wangwu@test.com']
};

client.query(query, (err, res) => {
    if (err) {
        console.log(err.stack);
    } else {
        console.log('预处理语句执行成功');
    }
});

给了name参数后,驱动会把这个查询存起来,下次同样name的查询可以直接复用。适合高频执行、结构固定的SQL。

3.4 参数传递的安全性

所有参数都要通过values数组传递,不要拼字符串。

// 正确写法
client.query('SELECT * FROM users WHERE name = $1', [userInput]);

// 错误写法
client.query(`SELECT * FROM users WHERE name = '${userInput}'`);

下面那种写法有SQL注入风险。userInput如果传'; DROP TABLE users; --,后果你懂的。

3.5 处理查询结果

res.rows是结果集数组,数组长度就是行数。

client.query('SELECT * FROM users WHERE age > $1', [18], (err, res) => {
    if (err) {
        console.error(err);
        return;
    }
    
    // 结果集为空的情况
    if (res.rows.length === 0) {
        console.log('没有找到符合条件的数据');
        return;
    }
    
    // 遍历结果
    res.rows.forEach(row => {
        console.log(`用户名: ${row.name}, 邮箱: ${row.email}`);
    });
});

四、关闭连接

用完数据库要关连接,不然连接会一直占着。

client.end(err => {
    if (err) {
        console.log('关闭连接时出错', err.stack);
    } else {
        console.log('连接已关闭');
    }
});

一个常见错误:在还没执行完查询的地方调用client.end(),会导致异步操作被中断。一定要等所有query的回调都执行完了再关。

五、完整示例

下面是一个完整的小程序,建表、插入、更新、查询、删除全流程都走一遍。

const { Client } = require('kb');

const client = new Client({
    user: 'SYSTEM',
    host: '127.0.0.1',
    database: 'TEST',
    password: '123456',
    port: 54321,
});

client.connect();

// 建表
client.query('CREATE TABLE test_node (id int, name char(100), address varchar(200), salary real, created_at DATE, description TEXT)', (err, res) => {
    if (err) {
        console.error('建表失败', err.stack);
        return;
    }
    console.log('建表成功');

    // 插入数据
    const now = new Date();
    const text = 'INSERT INTO test_node VALUES($1, $2, $3, $4, $5, $6) RETURNING *';
    const values = [1, '张三', '西湖区', 8000, now, '测试数据'];

    client.query(text, values, (err, res) => {
        if (err) {
            console.error('插入失败', err.stack);
            return;
        }
        console.log('插入成功', res.rows[0]);

        // 更新数据
        const updateQuery = {
            text: 'UPDATE test_node SET name = $1, address = $2 WHERE id = $3 RETURNING *',
            values: ['李四', '滨江区', 1]
        };

        client.query(updateQuery, (err, res) => {
            if (err) {
                console.error('更新失败', err.stack);
                return;
            }
            console.log('更新成功', res.rows[0]);

            // 查询数据
            client.query('SELECT * FROM test_node', (err, res) => {
                if (err) {
                    console.error('查询失败', err.stack);
                    return;
                }
                console.log('查询结果:', res.rows);

                // 删除数据
                client.query('DELETE FROM test_node WHERE id = 1', (err, res) => {
                    if (err) {
                        console.error('删除失败', err.stack);
                        return;
                    }
                    console.log('删除成功,影响行数:', res.rowCount);

                    // 删表
                    client.query('DROP TABLE test_node', (err, res) => {
                        if (err) {
                            console.error('删表失败', err.stack);
                            return;
                        }
                        console.log('删表成功');

                        client.end();
                    });
                });
            });
        });
    });
});

六、常见问题

6.1 模块找不到

Cannot find module 'kb'

检查node_modules目录里有没有kb文件夹,确认拷贝到了项目根目录。

6.2 连接被拒绝

connect ECONNREFUSED 127.0.0.1:54321

ksql命令行测试一下能不能连。如果命令行也连不上,检查IP、端口、用户名、密码。金仓的默认端口是54321,别写成5432。

6.3 Node.js版本不兼容

报一些奇怪的错,比如Class extends value undefined

降Node.js版本到12试试。驱动基于10.19.0开发,12比较稳,再高可能有问题。

6.4 中文乱码

数据库连接参数里没指定编码。建库时用UTF8,或者在连接后执行SET client_encoding TO 'UTF8'

client.query("SET client_encoding TO 'UTF8'", (err) => {
    // 继续后续操作
});

七、小结

上篇主要讲了Node.js连接金仓数据库的基础配置和CRUD操作:

  1. 驱动安装:下载驱动包,把kb文件夹放到项目的node_modules
  2. 版本要求:Node.js 8-12能跑,高了可能报错
  3. 基本操作Client类建连接,query方法执行SQL
  4. 参数传递:用$1, $2占位符,不要拼接字符串
  5. 连接管理:用完要end(),不然连接会一直占着

下篇会讲一些高级用法:连接池、事务、批量插入、流式查询等。希望对你有帮助。