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 / 0true / false                     │
  ├────────────┼───────────────────────────┼──────────────────────────────────┤
  │ JSON字段   │ JSON                      │ JSONB(更高效,支持索引)        │
  ├────────────┼───────────────────────────┼──────────────────────────────────┤
  │ 默认端口   │ 330654321                            │
  ├────────────┼───────────────────────────┼──────────────────────────────────┤
  │ 默认用户   │ root                      │ system                           │
  └────────────┴───────────────────────────┴──────────────────────────────────┘

  核心原则:金仓基于 PostgreSQL,用 pdo_pgsql 驱动直连,重点处理布尔值、自增ID、大小写、日期函数这四个差异点,其余 SQL
  基本兼容。

更多推荐