@ghini/kit
Version:
js practical tools to assist efficient development
301 lines (285 loc) • 9.55 kB
JavaScript
// pg.js
import pg from "pg";
import { insert } from "./insert.js";
import { truncate } from "./del.js";
const { Pool } = pg;
const instanceCache = new Map();
// --- 配置与缓存 ---
const defaultConfig = {
user: process.env.PGUSER || "postgres",
password: process.env.PGPASSWORD || "postgres",
host: process.env.PGHOST || "localhost",
port: process.env.PGPORT || 5432,
database: process.env.PGDATABASE || "postgres",
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
};
function createCacheKey(config) {
const normalized = { ...defaultConfig, ...config };
const keys = Object.keys(normalized).sort();
return keys.map((key) => `${key}:${normalized[key]}`).join("|");
}
// --- 优雅关闭 ---
const gracefulShutdown = async (signal) => {
console.log(`收到${signal}信号,开始优雅关闭...`);
await xpg.closeAll();
console.log("所有连接池已关闭");
process.exit(0);
};
["SIGINT", "SIGTERM"].forEach((signal) =>
process.on(signal, () => gracefulShutdown(signal))
);
class PGClient {
constructor(config = {}) {
const finalConfig = { ...defaultConfig, ...config };
this.config = finalConfig;
this.pool = new Pool(finalConfig);
// --- 事件监听 ---
this.pool.on("error", (err) =>
console.error("PG Pool Error:", {
message: err.message,
code: err.code,
database: finalConfig.database,
host: finalConfig.host,
})
);
// [优化4] 只在非生产环境记录连接日志,避免生产环境噪音
if (process.env.NODE_ENV !== "production") {
this.pool.on("connect", (client) => {
console.log(`数据库连接已建立 (PID: ${client.processID})`);
});
}
// --- 方法绑定 ---
/**
* 高性能批量插入或更新(Upsert)数据。
*
* @param {object} pg - pg客户端实例。
* @param {string} table - 表名。
* @param {object|object[]} data - 要插入的数据。
* @param {object} [options] - 选项。
* @param {string|Array} [options.onconflict] - 冲突处理配置, 类型决定行为:
* - **`string`**: 冲突时跳过 (DO NOTHING)。
* `{ onconflict: 'id' }`
* - **`Array`**: 冲突时更新 (DO UPDATE)。
* `['target', ...updateColumns]`
* - `target`: (string) 冲突列, 多个用逗号隔开, e.g., `'id'` 或 `'user_id,post_id'`。
* - `...updateColumns`: (string[]) 要更新的列。若省略, 则更新所有非冲突列。
*
* @example
* // 跳过冲突
* await insert(pg, 't', data, { onconflict: 'cid' });
*
* // 冲突时更新所有非冲突列
* await insert(pg, 't', data, { onconflict: ['cid'] });
*
* // 冲突时只更新指定列
* await insert(pg, 't', data, { onconflict: ['cid', 'exp_date', 'cvv'] });
*
* // 复合键冲突
* await insert(pg, 't', data, { onconflict: ['user_id,post_id', 'updated_at'] });
*
* @returns {Promise<[Error, null] | [null, import("pg").QueryResult<any>]>} 返回一个元组。
*/
this.insert = (table, data, options = {}) =>
insert(this, table, data, options);
this.truncate = (table) => truncate(this, table);
}
// --- 核心方法 ---
/**
* 执行单条原生SQL。这是执行任何非事务性查询的首选。
* @param {string} text - The SQL query text.
* @param {any[]} [params] - The parameters for the query.
* @returns {Promise<[Error, null] | [null, import("pg").QueryResult<any>]>}
*/
async query(text, params = []) {
const startTime = Date.now();
try {
const result = await this.pool.query(text, params);
const duration = Date.now() - startTime;
if (duration > 1000) {
console.warn("慢查询检测:", {
duration: `${duration}ms`,
query: text.length > 200 ? text.substring(0, 200) + "..." : text,
rowCount: result.rowCount,
});
}
return [null, result];
} catch (err) {
const duration = Date.now() - startTime;
// console.error("PG Query Error:", {
// message: err.message,
// code: err.code,
// severity: err.severity,
// detail: err.detail,
// hint: err.hint,
// query: text.length > 250 ? text.substring(0, 250) + "..." : text,
// params: params?.length > 0 ? params : undefined,
// duration: `${duration}ms`,
// });
return [err, null];
}
}
/**
* 获取一个专用的客户端,用于手动控制事务。
* **重要**: 调用者必须在 finally 块中调用 client.release()。
* @returns {Promise<[Error, null] | [null, import("pg").PoolClient]>}
*/
async getClient() {
try {
return [null, await this.pool.connect()];
} catch (err) {
console.error("获取数据库客户端失败:", {
message: err.message,
code: err.code,
poolStatus: this.getPoolStatus(),
});
return [err, null];
}
}
// --- 辅助方法 ---
/**
* 以安全的方式执行事务,自动处理 BEGIN, COMMIT, ROLLBACK 和 client.release()。
* @template T
* @param {(client: import("pg").PoolClient) => Promise<T>} callback - 在事务中执行的异步函数
* @param {{isolationLevel?: 'READ COMMITTED' | 'REPEATABLE READ' | 'SERIALIZABLE'}} [options] - 事务选项
* @returns {Promise<[Error, null] | [null, T]>} 返回一个元组,包含错误或回调函数的返回值
*/
async transaction(callback, options = {}) {
const [err, client] = await this.getClient();
if (err) return [err, null];
try {
await client.query("BEGIN");
// [优化3] 支持设置事务隔离级别
if (options.isolationLevel) {
await client.query(
`SET TRANSACTION ISOLATION LEVEL ${options.isolationLevel}`
);
}
const result = await callback(client);
await client.query("COMMIT");
return [null, result];
} catch (error) {
try {
await client.query("ROLLBACK");
console.log("事务已回滚");
} catch (rollbackErr) {
console.error("事务回滚失败:", rollbackErr);
}
return [error, null];
} finally {
client.release();
}
}
/**
* [优化1] 以原子方式批量执行多个查询(在单个事务中)。
* @param {Array<{text: string, params?: any[]}>} mquery - 要执行的查询数组
* @returns {Promise<[Error, null] | [null, import("pg").QueryResult<any>[]]>} 返回一个元组,包含错误或所有查询结果的数组
*/
async mquery(mquery) {
if (!Array.isArray(mquery) || mquery.length === 0) {
return [null, []];
}
return this.transaction(async (client) => {
const results = [];
for (const query of mquery) {
const result = await client.query(query.text, query.params || []);
results.push(result);
}
return results;
});
}
// --- 其他工具方法 ---
/**
* 获取连接池状态
* @returns {Object} 连接池状态信息
*/
getPoolStatus() {
return {
totalCount: this.pool.totalCount,
idleCount: this.pool.idleCount,
waitingCount: this.pool.waitingCount,
config: {
max: this.config.max,
database: this.config.database,
host: this.config.host,
port: this.config.port,
},
};
}
/**
* 测试数据库连接
* @returns {Promise<[Error, null] | [null, boolean]>} 返回连接测试结果
*/
async testConnection() {
const [err, result] = await this.query("SELECT 1 as test");
if (err) return [err, null];
return [null, result.rows[0]?.test === 1];
}
/**
* 关闭连接池
*/
async close() {
if (this.pool.ended) {
console.log("连接池已经关闭");
return;
}
try {
await this.pool.end();
console.log("连接池已关闭");
} catch (err) {
console.error("关闭连接池失败:", err);
throw err;
}
}
}
// --- 工厂函数与静态方法 ---
/**
* xpg 工厂函数 - 获取或创建PGClient实例
* @param {Object} config - 数据库配置
* @returns {PGClient} PGClient实例
*/
function xpg(config = {}) {
const cacheKey = createCacheKey(config);
if (!instanceCache.has(cacheKey)) {
const client = new PGClient(config);
instanceCache.set(cacheKey, client);
console.log(
`创建新的PG客户端实例: ${client.config.database}@${client.config.host}`
);
}
return instanceCache.get(cacheKey);
}
/**
* 以安全的方式执行事务的静态便捷方法。
* @template T
* @param {(client: import("pg").PoolClient) => Promise<T>} callback - 事务回调
* @param {Object} [config] - 数据库配置
* @returns {Promise<[Error, null] | [null, T]>} - [优化2] 返回值与库的其他部分保持一致
*/
xpg.transaction = async (callback, config = {}) => {
const instance = xpg(config);
return instance.transaction(callback);
};
/**
* 获取所有活跃实例的状态
* @returns {Array} 所有实例的状态信息
*/
xpg.getAllInstancesStatus = () => {
return Array.from(instanceCache.entries()).map(([key, client]) => ({
cacheKey: key,
status: client.getPoolStatus(),
}));
};
/**
* 关闭所有实例
*/
xpg.closeAll = async () => {
const closePromises = Array.from(instanceCache.values()).map((client) =>
client.close()
);
await Promise.allSettled(closePromises);
instanceCache.clear();
console.log("所有PG实例已关闭并清空缓存");
};
export { xpg };