PHP信创=PHP对接人大金仓数据库驱动开发与业务适配
·
PHP 对接人大金仓 KingbaseES 完整适配方案
先搞清楚人大金仓是什么
人大金仓 KingbaseES 本质上是基于 PostgreSQL 内核二次开发的国产数据库。这意味着:
好消息:PHP 的 pdo_pgsql 扩展可以直接连它,不需要专用驱动
坏消息:它有三种兼容模式,每种模式 SQL 语法不一样,要搞清楚你的库是哪种模式
三种兼容模式:
┌─────────────┬──────────────┬──────────┬────────────────────────────┐
│ 模式 │ 适合从哪迁移 │ 端口默认 │ 特点 │
├─────────────┼──────────────┼──────────┼────────────────────────────┤
│ PG 模式 │ PostgreSQL │ 54321 │ 最原生,功能最全 │
├─────────────┼──────────────┼──────────┼────────────────────────────┤
│ Oracle 模式 │ Oracle │ 54321 │ 支持 PL/SQL、序列、ROWNUM │
├─────────────┼──────────────┼──────────┼────────────────────────────┤
│ MySQL 模式 │ MySQL │ 54321 │ 支持反引号、AUTO_INCREMENT │
└─────────────┴──────────────┴──────────┴────────────────────────────┘
---
整体流程
1. 确认金仓兼容模式 → 决定后续所有 SQL 写法
2. 安装 PHP 驱动 → pdo_pgsql + pgsql 扩展
3. 建立连接 → DSN 格式和 MySQL 不同
4. 适配 SQL 差异 → 自增/分页/字符串函数/日期
5. 封装数据库层 → 屏蔽底层差异,业务代码不感知
6. 框架集成 → Laravel / ThinkPHP / Hyperf
7. 常见问题处理 → 大小写/引号/类型转换
---
第一步:确认金仓兼容模式
# 连接金仓数据库(用金仓自带的 ksql 客户端)
ksql -U system -d test -h 127.0.0.1 -p 54321
# 进入后查询当前数据库的兼容模式
SHOW db_compatibility;
# 或者
SELECT current_setting('db_compatibility');
# 输出可能是:
# PG → PostgreSQL 模式
# ORACLE → Oracle 模式
# MYSQL → MySQL 模式
# 如果要创建 MySQL 兼容模式的数据库
CREATE DATABASE myapp WITH db_compatibility='MYSQL' ENCODING='UTF8';
# 创建 Oracle 兼容模式
CREATE DATABASE myapp WITH db_compatibility='ORACLE' ENCODING='UTF8';
# 创建 PG 模式(默认)
CREATE DATABASE myapp ENCODING='UTF8';
---
第二步:安装 PHP 驱动
金仓基于 PostgreSQL,直接用 PHP 的 pdo_pgsql 扩展连接,不需要安装专用驱动。
# 方法1:如果 PHP 是从包管理器装的
# RHEL 系(OpenEuler/麒麟)
dnf install -y php-pdo php-pgsql php-pdo_pgsql
# Debian 系(UOS/Deepin)
apt install -y php-pgsql php-pdo-pgsql
# 方法2:如果 PHP 是从源码编译的
# 重新编译时加上这两个参数
./configure \
--with-pdo-pgsql=/usr/local/pgsql \ # 指向 PostgreSQL 客户端库路径
--with-pgsql=/usr/local/pgsql \
# ... 其他参数
# 方法3:用 pecl 单独装(不推荐,容易版本冲突)
pecl install pdo_pgsql
# 验证扩展是否加载
php -m | grep -E "pdo_pgsql|pgsql"
# 应该看到:
# pdo_pgsql
# pgsql
# 如果没有,手动在 php.ini 里加
echo "extension=pdo_pgsql.so" >> /usr/local/php/etc/conf.d/pgsql.ini
echo "extension=pgsql.so" >> /usr/local/php/etc/conf.d/pgsql.ini
特殊情况:金仓客户端库路径
# 如果服务器上装了金仓数据库,客户端库在金仓安装目录里
# 默认安装路径通常是 /opt/Kingbase/ES/V8/
ls /opt/Kingbase/ES/V8/lib/ | grep libpq
# 编译 PHP 时指向金仓的 libpq
./configure \
--with-pdo-pgsql=/opt/Kingbase/ES/V8 \
--with-pgsql=/opt/Kingbase/ES/V8
# 运行时确保能找到金仓的 libpq.so
echo "/opt/Kingbase/ES/V8/lib" >> /etc/ld.so.conf.d/kingbase.conf
ldconfig
---
第三步:建立连接
<?php
// db_connect.php - 连接人大金仓的基础示例
// PDO 连接金仓,DSN 格式和 MySQL 完全不同
// MySQL: mysql:host=127.0.0.1;dbname=test;charset=utf8mb4
// 金仓: pgsql:host=127.0.0.1;port=54321;dbname=test
$dsn = "pgsql:host=127.0.0.1;port=54321;dbname=myapp";
$username = "system"; // 金仓默认超级用户是 system
$password = "yourpassword";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 报错抛异常
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 返回关联数组
PDO::ATTR_PERSISTENT => false, // 不用持久连接
// 金仓特有:设置 schema 搜索路径
PDO::ATTR_INIT_COMMAND => "SET search_path TO public",
];
try {
$pdo = new PDO($dsn, $username, $password, $options);
echo "连接成功!\n";
// 验证连接和兼容模式
$stmt = $pdo->query("SELECT version(), current_setting('db_compatibility') as mode");
$info = $stmt->fetch();
echo "数据库版本: " . $info['version'] . "\n";
echo "兼容模式: " . $info['mode'] . "\n";
} catch (PDOException $e) {
echo "连接失败: " . $e->getMessage() . "\n";
}
---
第四步:SQL 语法差异适配(核心)
这是最重要的部分,从 MySQL 迁移过来的代码,这些地方必须改。
4.1 自增主键
-- MySQL 写法(金仓不支持)
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100)
);
-- 金仓 PG/Oracle 模式写法
CREATE TABLE users (
id SERIAL PRIMARY KEY, -- SERIAL = 自动创建序列
name VARCHAR(100)
);
-- 或者更明确的写法
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY, -- BIGSERIAL = 大整数自增
name VARCHAR(100)
);
-- 金仓 MySQL 兼容模式(可以用 AUTO_INCREMENT)
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100)
) ENGINE=InnoDB; -- MySQL 兼容模式下 ENGINE 会被忽略但不报错
<?php
// PHP 插入后获取自增 ID
// MySQL 用 lastInsertId(),金仓也支持,但要指定序列名
// 方法1:直接用 lastInsertId()(金仓 V8 支持)
$pdo->exec("INSERT INTO users (name) VALUES ('张三')");
$id = $pdo->lastInsertId(); // 可能返回空,不稳定
// 方法2:用 RETURNING 子句(推荐,最可靠)
$stmt = $pdo->prepare("INSERT INTO users (name) VALUES (?) RETURNING id");
$stmt->execute(['张三']);
$row = $stmt->fetch();
$id = $row['id']; // 稳定获取插入的 ID
// 方法3:指定序列名
$id = $pdo->lastInsertId('users_id_seq'); // 序列名规则:表名_字段名_seq
4.2 分页查询
-- MySQL 写法
SELECT * FROM users LIMIT 10 OFFSET 20;
SELECT * FROM users LIMIT 20, 10; -- 这种写法金仓不支持!
-- 金仓写法(PG/Oracle/MySQL 兼容模式都支持第一种)
SELECT * FROM users LIMIT 10 OFFSET 20;
-- 或者
SELECT * FROM users OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY; -- Oracle 风格
<?php
// PHP 分页封装
function paginate(PDO $pdo, string $table, int $page, int $size): array
{
$offset = ($page - 1) * $size;
// 用参数绑定,防 SQL 注入
// 注意:金仓的参数占位符用 $1 $2(PG风格)或 ? (也支持)
$stmt = $pdo->prepare(
"SELECT * FROM {$table} ORDER BY id LIMIT :limit OFFSET :offset"
);
$stmt->bindValue(':limit', $size, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
4.3 字符串函数差异
-- 字符串拼接
-- MySQL: CONCAT(a, b) 或 a + b(不推荐)
-- 金仓: CONCAT(a, b) 或 a || b(推荐)
-- 字符串截取
-- MySQL: SUBSTRING(str, 1, 3)
-- 金仓: SUBSTRING(str FROM 1 FOR 3) 或 SUBSTR(str, 1, 3)(两种都支持)
-- 字符串长度
-- MySQL: LENGTH(str) 返回字节数,CHAR_LENGTH 返回字符数
-- 金仓: LENGTH(str) 返回字符数(UTF8下),OCTET_LENGTH 返回字节数
-- 日期格式化
-- MySQL: DATE_FORMAT(now(), '%Y-%m-%d')
-- 金仓: TO_CHAR(now(), 'YYYY-MM-DD')
-- 当前时间
-- MySQL: NOW(), SYSDATE()
-- 金仓: NOW(), CURRENT_TIMESTAMP(SYSDATE 在 Oracle 模式下支持)
-- 判断空值
-- MySQL: IFNULL(val, default)
-- 金仓: COALESCE(val, default)(标准SQL,推荐)
-- NVL(val, default)(Oracle 模式支持)
-- 字符串转数字
-- MySQL: CAST('123' AS SIGNED)
-- 金仓: CAST('123' AS INTEGER) 或 '123'::INTEGER(PG风格类型转换)
4.4 表名和字段名大小写
-- 这是金仓(PostgreSQL)最坑的地方!
-- 金仓默认把不加引号的标识符转成小写
-- MySQL 里这样写没问题
SELECT UserName FROM Users; -- MySQL 不区分大小写
-- 金仓里,Users 会被转成 users,UserName 会被转成 username
-- 如果你的表名/字段名有大写,必须加双引号
SELECT "UserName" FROM "Users";
-- 最佳实践:建表时全用小写,避免这个问题
CREATE TABLE users (
id SERIAL PRIMARY KEY,
user_name VARCHAR(100), -- 用下划线分隔,不用驼峰
created_at TIMESTAMP DEFAULT NOW()
);
<?php
// PHP 里处理大小写问题
// 如果老系统表名有大写,用双引号包裹
$stmt = $pdo->query('SELECT "UserName", "Email" FROM "Users"');
// 更好的做法:建一个视图,把大写映射成小写
// CREATE VIEW users_view AS SELECT "UserName" as user_name FROM "Users";
4.5 布尔类型
-- MySQL 没有真正的布尔类型,用 TINYINT(1)
-- 金仓有原生 BOOLEAN 类型
-- 建表
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
is_published BOOLEAN DEFAULT FALSE, -- 金仓原生布尔
title VARCHAR(200)
);
-- 插入
INSERT INTO articles (title, is_published) VALUES ('测试文章', TRUE);
INSERT INTO articles (title, is_published) VALUES ('草稿', FALSE);
-- 查询
SELECT * FROM articles WHERE is_published = TRUE;
SELECT * FROM articles WHERE is_published; -- 简写,金仓支持
<?php
// PHP 里处理金仓布尔值
// 金仓返回的布尔值是字符串 't' 或 'f',不是 1/0
$stmt = $pdo->query("SELECT id, title, is_published FROM articles");
while ($row = $stmt->fetch()) {
// 金仓布尔值转 PHP bool
$isPublished = ($row['is_published'] === 't' || $row['is_published'] === true);
echo $row['title'] . ': ' . ($isPublished ? '已发布' : '草稿') . "\n";
}
// 插入布尔值
$stmt = $pdo->prepare("INSERT INTO articles (title, is_published) VALUES (?, ?)");
$stmt->execute(['新文章', 'true']); // 传字符串 'true'/'false'
// 或者
$stmt->execute(['新文章', true]); // 传 PHP bool,PDO 会自动转换
4.6 JSON 类型(金仓支持,比 MySQL 更强)
-- 建表
CREATE TABLE configs (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
settings JSONB -- JSONB 比 JSON 更高效,支持索引
);
-- 插入
INSERT INTO configs (name, settings)
VALUES ('app', '{"theme": "dark", "lang": "zh", "features": ["a", "b"]}');
-- 查询 JSON 字段
SELECT settings->>'theme' FROM configs WHERE name = 'app';
-- 输出: dark
-- 查询嵌套
SELECT settings->'features'->0 FROM configs WHERE name = 'app';
-- 输出: "a"
-- 条件查询
SELECT * FROM configs WHERE settings @> '{"theme": "dark"}';
<?php
// PHP 操作金仓 JSON
$stmt = $pdo->prepare(
"INSERT INTO configs (name, settings) VALUES (?, ?::jsonb)"
);
$stmt->execute([
'app',
json_encode(['theme' => 'dark', 'lang' => 'zh'])
]);
// 查询 JSON 字段
$stmt = $pdo->query(
"SELECT name, settings->>'theme' as theme FROM configs"
);
$rows = $stmt->fetchAll();
// settings 字段返回的是 JSON 字符串,需要 json_decode
---
第五步:封装数据库适配层
这是整个方案的核心,用一个类屏蔽金仓和 MySQL 的差异,业务代码不需要关心底层是哪个数据库。
<?php
// src/Database/KingbaseConnection.php
declare(strict_types=1);
namespace App\Database;
use PDO;
use PDOException;
use PDOStatement;
class KingbaseConnection
{
private PDO $pdo;
private string $compatMode; // PG / ORACLE / MYSQL
public function __construct(array $config)
{
$dsn = sprintf(
"pgsql:host=%s;port=%d;dbname=%s",
$config['host'],
$config['port'] ?? 54321,
$config['database']
);
$this->pdo = new PDO(
$dsn,
$config['username'],
$config['password'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_PERSISTENT => false,
]
);
// 设置字符集和 schema
$this->pdo->exec("SET client_encoding TO 'UTF8'");
$this->pdo->exec("SET search_path TO " . ($config['schema'] ?? 'public'));
// 获取兼容模式
$stmt = $this->pdo->query("SELECT current_setting('db_compatibility')");
$this->compatMode = strtoupper($stmt->fetchColumn() ?: 'PG');
}
// ==================== 基础查询 ====================
public function query(string $sql, array $params = []): array
{
$stmt = $this->execute($sql, $params);
return $stmt->fetchAll();
}
public function queryOne(string $sql, array $params = []): ?array
{
$stmt = $this->execute($sql, $params);
$row = $stmt->fetch();
return $row ?: null;
}
public function queryValue(string $sql, array $params = []): mixed
{
$stmt = $this->execute($sql, $params);
return $stmt->fetchColumn();
}
// ==================== 写操作 ====================
/**
* 插入一条记录,返回自增 ID
* 用 RETURNING 子句,比 lastInsertId() 更可靠
*/
public function insert(string $table, array $data): int|string
{
$columns = array_keys($data);
$placeholders = array_map(fn($col) => ':' . $col, $columns);
// 用双引号包裹列名,避免大小写问题
$colStr = implode(', ', array_map(fn($c) => '"' . $c . '"', $columns));
$valStr = implode(', ', $placeholders);
$sql = "INSERT INTO \"{$table}\" ({$colStr}) VALUES ({$valStr}) RETURNING id";
$stmt = $this->execute($sql, $data);
$row = $stmt->fetch();
return $row['id'] ?? 0;
}
/**
* 更新记录,返回影响行数
*/
public function update(string $table, array $data, array $where): int
{
$setClauses = [];
$params = [];
foreach ($data as $col => $val) {
$setClauses[] = '"' . $col . '" = :set_' . $col;
$params['set_' . $col] = $val;
}
$whereClauses = [];
foreach ($where as $col => $val) {
$whereClauses[] = '"' . $col . '" = :where_' . $col;
$params['where_' . $col] = $val;
}
$sql = sprintf(
'UPDATE "%s" SET %s WHERE %s',
$table,
implode(', ', $setClauses),
implode(' AND ', $whereClauses)
);
$stmt = $this->execute($sql, $params);
return $stmt->rowCount();
}
/**
* 删除记录,返回影响行数
*/
public function delete(string $table, array $where): int
{
$whereClauses = [];
foreach ($where as $col => $val) {
$whereClauses[] = '"' . $col . '" = :' . $col;
}
$sql = sprintf(
'DELETE FROM "%s" WHERE %s',
$table,
implode(' AND ', $whereClauses)
);
$stmt = $this->execute($sql, $where);
return $stmt->rowCount();
}
// ==================== 分页查询 ====================
public function paginate(
string $sql,
array $params,
int $page,
int $size
): array {
// 查总数
$countSql = "SELECT COUNT(*) FROM ({$sql}) AS _count_query";
$total = (int) $this->queryValue($countSql, $params);
// 查数据
$offset = ($page - 1) * $size;
$dataSql = $sql . " LIMIT :_limit OFFSET :_offset";
$params['_limit'] = $size;
$params['_offset'] = $offset;
$data = $this->query($dataSql, $params);
return [
'data' => $data,
'total' => $total,
'page' => $page,
'size' => $size,
'total_page' => (int) ceil($total / $size),
];
}
// ==================== 事务 ====================
public function transaction(callable $callback): mixed
{
$this->pdo->beginTransaction();
try {
$result = $callback($this);
$this->pdo->commit();
return $result;
} catch (\Throwable $e) {
$this->pdo->rollBack();
throw $e;
}
}
// ==================== 工具方法 ====================
/**
* 把金仓返回的布尔值统一转成 PHP bool
* 金仓返回 't'/'f',MySQL 返回 1/0
*/
public static function toBool(mixed $value): bool
{
if (is_bool($value)) return $value;
if ($value === 't' || $value === 'true' || $value === '1') return true;
return false;
}
/**
* 获取兼容模式
*/
public function getCompatMode(): string
{
return $this->compatMode;
}
public function getPdo(): PDO
{
return $this->pdo;
}
// ==================== 私有方法 ====================
private function execute(string $sql, array $params = []): PDOStatement
{
$stmt = $this->pdo->prepare($sql);
foreach ($params as $key => $value) {
// 自动判断参数类型
$type = match (true) {
is_int($value) => PDO::PARAM_INT,
is_bool($value) => PDO::PARAM_BOOL,
is_null($value) => PDO::PARAM_NULL,
default => PDO::PARAM_STR,
};
// 支持 :name 和 ? 两种占位符
$bindKey = is_string($key) ? ':' . ltrim($key, ':') : ($key + 1);
$stmt->bindValue($bindKey, $value, $type);
}
$stmt->execute();
return $stmt;
}
}
---
第六步:业务层完整示例
<?php
// src/Repository/UserRepository.php
// 用户数据访问层,展示完整的 CRUD
declare(strict_types=1);
namespace App\Repository;
use App\Database\KingbaseConnection;
class UserRepository
{
public function __construct(
private KingbaseConnection $db
) {}
// 建表 SQL(在初始化脚本里执行)
public function createTable(): void
{
$this->db->getPdo()->exec("
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
real_name VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
role VARCHAR(20) DEFAULT 'user',
extra JSONB,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
");
// 创建索引
$this->db->getPdo()->exec("
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at);
");
}
// 创建用户
public function create(array $data): int
{
// 密码必须加密,绝不明文存储
$data['password'] = password_hash($data['password'], PASSWORD_BCRYPT);
$data['created_at'] = date('Y-m-d H:i:s');
$data['updated_at'] = date('Y-m-d H:i:s');
// extra 字段是 JSONB,需要转成 JSON 字符串
if (isset($data['extra']) && is_array($data['extra'])) {
$data['extra'] = json_encode($data['extra'], JSON_UNESCAPED_UNICODE);
}
return (int) $this->db->insert('users', $data);
}
// 根据 ID 查询
public function findById(int $id): ?array
{
$row = $this->db->queryOne(
"SELECT id, username, email, real_name, is_active, role, extra, created_at
FROM users WHERE id = :id",
['id' => $id]
);
if ($row) {
$row = $this->formatRow($row);
}
return $row;
}
// 根据邮箱查询(用于登录)
public function findByEmail(string $email): ?array
{
return $this->db->queryOne(
"SELECT * FROM users WHERE email = :email AND is_active = TRUE",
['email' => $email]
);
}
// 分页列表
public function list(int $page = 1, int $size = 20, array $filters = []): array
{
$sql = "SELECT id, username, email, real_name, is_active, role, created_at
FROM users WHERE 1=1";
$params = [];
// 动态条件
if (!empty($filters['role'])) {
$sql .= " AND role = :role";
$params['role'] = $filters['role'];
}
if (!empty($filters['keyword'])) {
// 金仓支持 ILIKE(大小写不敏感的 LIKE)
$sql .= " AND (username ILIKE :kw OR email ILIKE :kw OR real_name ILIKE :kw)";
$params['kw'] = '%' . $filters['keyword'] . '%';
}
if (isset($filters['is_active'])) {
$sql .= " AND is_active = :is_active";
$params['is_active'] = $filters['is_active'] ? 'true' : 'false';
}
$sql .= " ORDER BY created_at DESC";
$result = $this->db->paginate($sql, $params, $page, $size);
// 格式化每一行
$result['data'] = array_map([$this, 'formatRow'], $result['data']);
return $result;
}
// 更新用户
public function update(int $id, array $data): bool
{
// 不允许直接更新密码(走专门的修改密码接口)
unset($data['password'], $data['id']);
$data['updated_at'] = date('Y-m-d H:i:s');
if (isset($data['extra']) && is_array($data['extra'])) {
$data['extra'] = json_encode($data['extra'], JSON_UNESCAPED_UNICODE);
}
$affected = $this->db->update('users', $data, ['id' => $id]);
return $affected > 0;
}
// 修改密码
public function changePassword(int $id, string $newPassword): bool
{
$affected = $this->db->update(
'users',
[
'password' => password_hash($newPassword, PASSWORD_BCRYPT),
'updated_at' => date('Y-m-d H:i:s'),
],
['id' => $id]
);
return $affected > 0;
}
// 软删除(设置 is_active = false)
public function softDelete(int $id): bool
{
$affected = $this->db->update(
'users',
['is_active' => 'false', 'updated_at' => date('Y-m-d H:i:s')],
['id' => $id]
);
return $affected > 0;
}
// 统计
public function count(array $filters = []): int
{
$sql = "SELECT COUNT(*) FROM users WHERE 1=1";
$params = [];
if (isset($filters['is_active'])) {
$sql .= " AND is_active = :is_active";
$params['is_active'] = $filters['is_active'] ? 'true' : 'false';
}
return (int) $this->db->queryValue($sql, $params);
}
// 格式化行数据(处理金仓特有的类型)
private function formatRow(array $row): array
{
// 布尔值:金仓返回 't'/'f'
if (isset($row['is
✻ Churned for 2m 23s
> 继续
✻ Twisting… ●T // 布尔值:金仓返回 't'/'f'
if (isset($row['is_active'])) {
$row['is_active'] = KingbaseConnection::toBool($row['is_active']);
}
// JSONB 字段:返回的是 JSON 字符串,转成数组 if (isset($row['extra']) && is_string($row['extra'])) {
$row['extra'] = json_decode($row['extra'], true) ?? [];
}
// 时间格式统一
if (isset($row['created_at'])) {
$row['created_at'] = date('Y-m-d H:i:s', strtotime($row['created_at']));
}
return $row;
}
}
---
第七步:框架集成
7.1 Laravel 集成
Laravel 原生支持 PostgreSQL,金仓直接复用,改几个配置就行。
<?php
// config/database.php - 在 connections 数组里加金仓配置
return [
'default' => env('DB_CONNECTION', 'kingbase'),
'connections' => [
// 金仓连接配置
'kingbase' => [
'driver' => 'pgsql', // 用 pgsql 驱动
'host' => env('KB_HOST', '127.0.0.1'),
'port' => env('KB_PORT', '54321'),
'database' => env('KB_DATABASE', 'myapp'),
'username' => env('KB_USERNAME', 'system'),
'password' => env('KB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
// 如果还要保留 MySQL 连接
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'myapp'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
],
],
];
# .env 文件
DB_CONNECTION=kingbase
KB_HOST=127.0.0.1
KB_PORT=54321
KB_DATABASE=myapp
KB_USERNAME=system
KB_PASSWORD=yourpassword
<?php
// Laravel Migration 适配金仓
// database/migrations/2024_01_01_create_users_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
// 指定用金仓连接
protected $connection = 'kingbase';
public function up(): void
{
Schema::connection('kingbase')->create('users', function (Blueprint $table) {
// Laravel 的 id() 在 pgsql 驱动下自动用 BIGSERIAL
$table->id();
$table->string('username', 50)->unique();
$table->string('email', 100)->unique();
$table->string('password');
$table->string('real_name', 50)->nullable();
$table->boolean('is_active')->default(true);
$table->string('role', 20)->default('user');
// JSONB 类型
$table->jsonb('extra')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::connection('kingbase')->dropIfExists('users');
}
};
<?php
// Laravel Model 适配金仓
// app/Models/User.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
class User extends Model
{
// 指定连接
protected $connection = 'kingbase';
protected $table = 'users';
protected $fillable = [
'username', 'email', 'password', 'real_name', 'is_active', 'role', 'extra'
];
protected $hidden = ['password'];
// 类型转换:自动处理金仓的布尔值和 JSONB
protected $casts = [
'is_active' => 'boolean', // 自动把 't'/'f' 转成 PHP bool
'extra' => 'array', // 自动把 JSONB 字符串转成数组
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
// 密码自动加密
protected function password(): Attribute
{
return Attribute::make(
set: fn($value) => password_hash($value, PASSWORD_BCRYPT)
);
}
// 作用域:只查活跃用户
public function scopeActive($query)
{
return $query->where('is_active', true);
}
// 作用域:按角色筛选
public function scopeRole($query, string $role)
{
return $query->where('role', $role);
}
}
<?php
// Laravel Controller 使用示例
// app/Http/Controllers/UserController.php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class UserController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = User::active();
if ($request->filled('keyword')) {
$kw = $request->input('keyword');
// 金仓支持 ILIKE,Laravel whereRaw 直接写
$query->whereRaw(
"username ILIKE ? OR email ILIKE ? OR real_name ILIKE ?",
["%{$kw}%", "%{$kw}%", "%{$kw}%"]
);
}
if ($request->filled('role')) {
$query->role($request->input('role'));
}
$users = $query->orderByDesc('created_at')
->paginate($request->input('size', 20));
return response()->json($users);
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'username' => 'required|string|max:50|unique:kingbase.users',
'email' => 'required|email|unique:kingbase.users',
'password' => 'required|string|min:8',
'real_name' => 'nullable|string|max:50',
]);
$user = User::create($validated);
return response()->json(['id' => $user->id, 'message' => '创建成功'], 201);
}
}
---
7.2 ThinkPHP 集成
<?php
// config/database.php
return [
'default' => 'kingbase',
'connections' => [
'kingbase' => [
'type' => 'pgsql', // ThinkPHP 内置支持 pgsql
'hostname' => env('KB_HOST', '127.0.0.1'),
'hostport' => env('KB_PORT', '54321'),
'database' => env('KB_DATABASE', 'myapp'),
'username' => env('KB_USERNAME', 'system'),
'password' => env('KB_PASSWORD', ''),
'charset' => 'utf8',
'schema' => 'public',
'prefix' => '',
'params' => [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
],
],
],
];
<?php
// app/model/User.php - ThinkPHP Model
namespace app\model;
use think\Model;
class User extends Model
{
protected $connection = 'kingbase';
protected $table = 'users';
protected $pk = 'id';
// 字段类型转换
protected $type = [
'is_active' => 'boolean',
'extra' => 'json',
];
// 写入时自动处理
protected function setPasswordAttr(string $value): string
{
return password_hash($value, PASSWORD_BCRYPT);
}
// 读取时自动处理布尔值
protected function getIsActiveAttr($value): bool
{
return $value === 't' || $value === true || $value === 1;
}
}
<?php
// ThinkPHP 控制器使用示例
namespace app\controller;
use app\model\User;
use think\Request;
class UserController
{
public function list(Request $request): array
{
$page = $request->param('page/d', 1);
$size = $request->param('size/d', 20);
$keyword = $request->param('keyword');
$query = User::where('is_active', true);
if ($keyword) {
// ThinkPHP whereRaw 写金仓原生 SQL
$query->whereRaw(
"username ILIKE :kw OR email ILIKE :kw",
['kw' => "%{$keyword}%"]
);
}
$result = $query->order('created_at', 'desc')
->paginate(['list_rows' => $size, 'page' => $page]);
return [
'code' => 0,
'data' => $result->items(),
'total' => $result->total(),
];
}
}
---
7.3 Hyperf 集成
<?php
// config/autoload/databases.php
return [
'default' => [
'driver' => 'pgsql',
'host' => env('KB_HOST', '127.0.0.1'),
'port' => (int) env('KB_PORT', 54321),
'database' => env('KB_DATABASE', 'myapp'),
'username' => env('KB_USERNAME', 'system'),
'password' => env('KB_PASSWORD', ''),
'charset' => 'utf8',
'schema' => 'public',
'pool' => [
'min_connections' => 5,
'max_connections' => 50,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => 60,
'max_idle_time' => 60.0,
],
],
];
<?php
// app/Model/User.php - Hyperf Model
namespace App\Model;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
protected ?string $connection = 'default';
protected string $table = 'users';
protected array $fillable = [
'username', 'email', 'password', 'real_name', 'is_active', 'role', 'extra'
];
protected array $hidden = ['password'];
protected array $casts = [
'is_active' => 'boolean',
'extra' => 'array',
];
}
---
第八步:数据迁移工具(MySQL → 金仓)
<?php
// scripts/migrate_mysql_to_kingbase.php
// 把 MySQL 数据迁移到金仓的脚本
declare(strict_types=1);
class MysqlToKingbaseMigrator
{
private PDO $mysql;
private PDO $kingbase;
public function __construct()
{
// 连 MySQL
$this->mysql = new PDO(
"mysql:host=127.0.0.1;port=3306;dbname=old_db;charset=utf8mb4",
'root', 'mysql_password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// 连金仓
$this->kingbase = new PDO(
"pgsql:host=127.0.0.1;port=54321;dbname=new_db",
'system', 'kb_password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
}
/**
* 迁移一张表的数据
* @param string $table 表名
* @param int $batchSize 每批处理多少条
*/
public function migrateTable(string $table, int $batchSize = 1000): void
{
echo "开始迁移表: {$table}\n";
// 获取总数
$total = $this->mysql->query("SELECT COUNT(*) FROM `{$table}`")->fetchColumn();
echo "总记录数: {$total}\n";
$offset = 0;
$migrated = 0;
while ($offset < $total) {
// 从 MySQL 分批读取
$rows = $this->mysql->query(
"SELECT * FROM `{$table}` LIMIT {$batchSize} OFFSET {$offset}"
)->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) break;
// 批量写入金仓
$this->kingbase->beginTransaction();
try {
foreach ($rows as $row) {
$row = $this->transformRow($table, $row);
$this->insertRow($table, $row);
}
$this->kingbase->commit();
} catch (\Throwable $e) {
$this->kingbase->rollBack();
echo "迁移失败 offset={$offset}: " . $e->getMessage() . "\n";
throw $e;
}
$migrated += count($rows);
$offset += $batchSize;
echo "进度: {$migrated}/{$total}\r";
}
// 迁移完成后,重置金仓的序列(自增ID从最大值+1开始)
$this->resetSequence($table);
echo "\n表 {$table} 迁移完成!\n";
}
/**
* 数据行转换:处理 MySQL 和金仓的类型差异
*/
private function transformRow(string $table, array $row): array
{
foreach ($row as $col => &$val) {
if ($val === null) continue;
// MySQL TINYINT(1) 的布尔值 → 金仓 BOOLEAN
// 根据实际字段名判断,这里列举常见的布尔字段名
if (in_array($col, ['is_active', 'is_deleted', 'is_enabled', 'status'])) {
$val = $val ? 'true' : 'false';
}
// MySQL datetime '0000-00-00 00:00:00' → NULL(金仓不接受这个值)
if (is_string($val) && $val === '0000-00-00 00:00:00') {
$val = null;
}
// MySQL JSON 字符串保持原样,金仓 JSONB 会自动解析
}
return $row;
}
/**
* 插入一行数据到金仓
*/
private function insertRow(string $table, array $row): void
{
$columns = array_keys($row);
$colStr = implode(', ', array_map(fn($c) => '"' . $c . '"', $columns));
$valStr = implode(', ', array_map(fn($c) => ':' . $c, $columns));
$stmt = $this->kingbase->prepare(
"INSERT INTO \"{$table}\" ({$colStr}) VALUES ({$valStr}) ON CONFLICT DO NOTHING"
);
// ON CONFLICT DO NOTHING:如果主键冲突就跳过,避免重复迁移报错
$stmt->execute($row);
}
/**
* 重置序列:迁移完数据后,让自增 ID 从最大值+1 开始
* 否则新插入数据会因为 ID 冲突报错
*/
private function resetSequence(string $table): void
{
$sql = "SELECT setval(
pg_get_serial_sequence('\"{$table}\"', 'id'),
COALESCE((SELECT MAX(id) FROM \"{$table}\"), 0) + 1,
false
)";
$this->kingbase->exec($sql);
echo "序列已重置\n";
}
}
// 执行迁移
$migrator = new MysqlToKingbaseMigrator();
$migrator->migrateTable('users');
$migrator->migrateTable('articles');
$migrator->migrateTable('orders');
---
第九步:常见报错速查
<?php
// 常见错误及处理方式,整理成注释供参考
/*
错误1: SQLSTATE[42P01]: Undefined table: 7 ERROR: relation "Users" does not exist
原因: 金仓把表名转成小写了,你的表名是大写
解决: 建表时用小写,或查询时加双引号 "Users"
错误2: SQLSTATE[42703]: column "userName" does not exist
原因: 同上,字段名大小写问题
解决: 字段名全用小写+下划线,或加双引号
错误3: SQLSTATE[22P02]: invalid input syntax for type boolean: "1"
原因: 金仓布尔字段不接受 1/0,要用 true/false
解决: 传 'true'/'false' 字符串,或 PHP true/false
错误4: SQLSTATE[23505]: duplicate key value violates unique constraint
原因: 主键或唯一键冲突,通常是序列没重置
解决: 执行 resetSequence(),或用 ON CONFLICT DO NOTHING
错误5: SQLSTATE[42883]: function date_format(timestamp, unknown) does not exist
原因: MySQL 的 DATE_FORMAT 在金仓里不存在
解决: 改用 TO_CHAR(field, 'YYYY-MM-DD HH24:MI:SS')
错误6: SQLSTATE[42601]: syntax error at or near "LIMIT"
原因: 用了 LIMIT 20, 10 这种 MySQL 特有语法
解决: 改成 LIMIT 10 OFFSET 20
错误7: could not connect to server: Connection refused (port 54321)
原因: 端口不对,金仓默认 54321,不是 MySQL 的 3306
解决: 检查 .env 里的端口配置
错误8: FATAL: password authentication failed for user "root"
原因: 金仓默认超级用户是 system,不是 root
解决: 用 system 用户,或创建对应用户
*/
---
第十步:一键验证脚本
<?php
// scripts/verify_kingbase.php
// 运行这个脚本验证金仓连接和基本功能是否正常
$config = [
'host' => '127.0.0.1',
'port' => 54321,
'database' => 'myapp',
'username' => 'system',
'password' => 'yourpassword',
];
echo "=== 人大金仓 PHP 连接验证 ===\n\n";
// 1. 测试连接
echo "1. 测试连接... ";
try {
$pdo = new PDO(
"pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}",
$config['username'],
$config['password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
echo "✓\n";
} catch (PDOException $e) {
echo "✗ " . $e->getMessage() . "\n";
exit(1);
}
// 2. 查版本和兼容模式
echo "2. 数据库信息:\n";
$row = $pdo->query("SELECT version() as ver, current_setting('db_compatibility') as mode")->fetch(PDO::FETCH_ASSOC);
echo " 版本: " . substr($row['ver'], 0, 50) . "\n";
echo " 兼容模式: " . $row['mode'] . "\n";
// 3. 测试建表
echo "3. 测试建表... ";
$pdo->exec("DROP TABLE IF EXISTS _php_test");
$pdo->exec("
CREATE TABLE _php_test (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(50),
flag BOOLEAN DEFAULT TRUE,
data JSONB,
created TIMESTAMP DEFAULT NOW()
)
");
echo "✓\n";
// 4. 测试插入(用 RETURNING 获取 ID)
echo "4. 测试插入... ";
$stmt = $pdo->prepare(
"INSERT INTO _php_test (name, flag, data) VALUES (?, ?, ?::jsonb) RETURNING id"
);
$stmt->execute(['测试数据', 'true', '{"key":"value"}']);
$id = $stmt->fetch()['id'];
echo "✓ (id={$id})\n";
// 5. 测试查询
echo "5. 测试查询... ";
$row = $pdo->query("SELECT * FROM _php_test WHERE id = {$id}")->fetch(PDO::FETCH_ASSOC);
$ok = $row['name'] === '测试数据';
echo ($ok ? "✓" : "✗") . "\n";
// 6. 测试布尔值
echo "6. 测试布尔值... ";
$flag = $row['flag'];
$ok = ($flag === 't' || $flag === true);
echo ($ok ? "✓ (返回值: '{$flag}')" : "✗") . "\n";
// 7. 测试 JSONB
echo "7. 测试 JSONB... ";
$data = json_decode($row['data'], true);
$ok = isset($data['key']) && $data['key'] === 'value';
echo ($ok ? "✓" : "✗") . "\n";
// 8. 测试分页
echo "8. 测试分页... ";
for ($i = 1; $i <= 5; $i++) {
$pdo->exec("INSERT INTO _php_test (name) VALUES ('记录{$i}')");
}
$rows = $pdo->query("SELECT * FROM _php_test ORDER BY id LIMIT 3 OFFSET 2")->fetchAll();
$ok = count($rows) === 3;
echo ($ok ? "✓" : "✗") . "\n";
// 9. 测试事务
echo "9. 测试事务回滚... ";
$before = $pdo->query("SELECT COUNT(*) FROM _php_test")->fetchColumn();
try {
$pdo->beginTransaction();
$pdo->exec("INSERT INTO _php_test (name) VALUES ('事务测试')");
$pdo->rollBack();
} catch (\Exception $e) {
$pdo->rollBack();
}
$after = $pdo->query("SELECT COUNT(*) FROM _php_test")->fetchColumn();
$ok = ($before == $after);
echo ($ok ? "✓" : "✗") . "\n";
// 10. 清理
$pdo->exec("DROP TABLE _php_test");
echo "10. 清理测试表... ✓\n";
echo "\n=== 所有验证通过,金仓连接正常 ===\n";
---
总结速查表
┌────────────┬───────────────────────────┬──────────────────────────────────┐
│ 场景 │ MySQL 写法 │ 金仓写法 │
├────────────┼───────────────────────────┼──────────────────────────────────┤
│ 自增主键 │ AUTO_INCREMENT │ BIGSERIAL │
├────────────┼───────────────────────────┼──────────────────────────────────┤
│ 获取插入ID │ lastInsertId() │ RETURNING id │
├────────────┼───────────────────────────┼──────────────────────────────────┤
│ 分页 │ LIMIT 20, 10 │ LIMIT 10 OFFSET 20 │
├────────────┼───────────────────────────┼──────────────────────────────────┤
│ 日期格式化 │ DATE_FORMAT(d,'%Y-%m-%d') │ TO_CHAR(d,'YYYY-MM-DD') │
├────────────┼───────────────────────────┼──────────────────────────────────┤
│ 空值替换 │ IFNULL(v, 0) │ COALESCE(v, 0) │
├────────────┼───────────────────────────┼──────────────────────────────────┤
│ 模糊查询 │ LIKE '%x%' │ ILIKE '%x%'(不区分大小写) │
├────────────┼───────────────────────────┼──────────────────────────────────┤
│ 字符串拼接 │ CONCAT(a,b) │ a || b 或 CONCAT(a,b) │
├────────────┼───────────────────────────┼──────────────────────────────────┤
│ 类型转换 │ CAST(x AS SIGNED) │ x::INTEGER 或 CAST(x AS INTEGER) │
├────────────┼───────────────────────────┼──────────────────────────────────┤
│ 布尔值 │ 1 / 0 │ true / false │
├────────────┼───────────────────────────┼──────────────────────────────────┤
│ JSON字段 │ JSON │ JSONB(更高效,支持索引) │
├────────────┼───────────────────────────┼──────────────────────────────────┤
│ 默认端口 │ 3306 │ 54321 │
├────────────┼───────────────────────────┼──────────────────────────────────┤
│ 默认用户 │ root │ system │
└────────────┴───────────────────────────┴──────────────────────────────────┘
核心原则:金仓基于 PostgreSQL,用 pdo_pgsql 驱动直连,重点处理布尔值、自增ID、大小写、日期函数这四个差异点,其余 SQL
基本兼容。
更多推荐


所有评论(0)