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

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合并一下就行。

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

perl 复制代码
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模块很像,用过的人应该不陌生。

javascript 复制代码
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处理了一下:

javascript 复制代码
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。这个方法是异步的,需要传入回调函数。

javascript 复制代码
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注入。

方式一:分开传参

javascript 复制代码
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]);
    }
});

方式二:用对象传参

javascript 复制代码
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模板,多次执行时只需要传参数,效率更高。

javascript 复制代码
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数组传递,不要拼字符串。

javascript 复制代码
// 正确写法
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是结果集数组,数组长度就是行数。

javascript 复制代码
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}`);
    });
});

四、关闭连接

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

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

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

五、完整示例

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

javascript 复制代码
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'。

javascript 复制代码
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(),不然连接会一直占着

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

相关推荐
小林ixn1 分钟前
用 FastAPI + SQLAlchemy 从零手搓一个博客系统:一套能落地、能讲清楚的 RESTful 后端
后端·python·fastapi
不识君9 分钟前
esl-client 2.0.0 发布:Java 项目接入 FreeSWITCH,从引入到使用
spring boot·后端·开源
牧瀬クリスだ25 分钟前
Redis五大数据类型全解析:从入门到实战
java·数据库·redis·后端·缓存
Rain的Java大神之路44 分钟前
北漂 13 年,我把 Java 家底钉成了这张知识图谱(全站免费)
java·后端·程序员
Amos_Web1 小时前
通过 Rspack 源码学 Rust(一):`mod`、`pub` 与 `use crate`/`use super`
后端
张彦峰ZYF1 小时前
拆得开不等于该拆开:微服务与模块化单体的边界判据、规模阈值、团队适配与一致性代价
后端·微服务·边界是否稳定·扩展天花板是否到顶·有无小组能独立值班·独立部署的自由会否被用上·这一刀是否切断不可降级的不变量
是江迪呀1 小时前
介绍一个开源项目 —— 烂笔头
前端·后端
captain3761 小时前
Spring Web MVC(2)
java·前端·spring boot·后端·spring·mvc
紧跟先前的步伐1 小时前
Go AI Agent 实战 17|模型适配层设计:让模型可替换
开发语言·人工智能·后端·ai·golang
IT_陈寒2 小时前
一文搞懂Java的核心概念
前端·人工智能·后端