数据库与持久化

响应式 PostgreSQL 客户端(Reactive PostgreSQL Client)

Reactive PostgreSQL Client 是一款针对 PostgreSQL 的客户端,提供直观易用的 API,专注于高伸缩性与低开销。

该客户端采用响应式与非阻塞设计,允许仅凭单个线程即可处理大量数据库连接。

  • 特性列表

    • 事件驱动

    • 轻量高效

    • 内置连接池

    • 预编译查询(Prepared queries)缓存

    • 基于 PostgreSQL NOTIFY/LISTEN 的发布 / 订阅

    • 批量操作与游标

    • 行流式处理(Row streaming)

    • 命令流水线(Command pipelining)

    • RxJava API

    • 零多余拷贝,直接从内存映射到对象

    • 支持 Java 8 日期和时间类型

    • 支持 SSL/TLS

    • 支持 Unix Domain Socket

    • 支持 HTTP/1.x CONNECT、SOCKS4a 或 SOCKS5 代理

    • 支持 4 层与 7 层代理(Proxy)

引入依赖(Usage)

要使用 Reactive PostgreSQL Client,请将以下依赖项添加到构建描述文件的 dependencies 部分:

  • Maven(在 pom.xml 中):

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-pg-client</artifactId>
  <version>5.2.1</version>
</dependency>
  • Gradle(在 build.gradle 文件中):

dependencies {
  compile 'io.vertx:vertx-pg-client:5.2.1'
}

快速入门(Getting started)

以下是连接、查询以及断开连接的最简单示例:

PgConnectOptions connectOptions = new PgConnectOptions()
  .setPort(5432)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret");

// 连接池选项
PoolOptions poolOptions = new PoolOptions()
  .setMaxSize(5);

// 创建客户端连接池
SqlClient client = PgBuilder
  .client()
  .with(poolOptions)
  .connectingTo(connectOptions)
  .build();

// 执行简单查询
client
  .query("SELECT * FROM users WHERE id='julien'")
  .execute()
  .onComplete(ar -> {
    if (ar.succeeded()) {
      RowSet<Row> result = ar.result();
      System.out.println("Got " + result.size() + " rows ");
    } else {
      System.out.println("Failure: " + ar.cause().getMessage());
    }

    // 关闭连接池
    client.close();
  });

连接至 PostgreSQL 数据库

大多数情况下,你会使用连接池来连接 PostgreSQL:

PgConnectOptions connectOptions = new PgConnectOptions()
  .setPort(5432)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret");

// 连接池选项
PoolOptions poolOptions = new PoolOptions()
  .setMaxSize(5);

// 创建池化客户端
SqlClient client = PgBuilder
  .client()
  .with(poolOptions)
  .connectingTo(connectOptions)
  .build();

池化客户端底层使用连接池,执行任何操作时都会从连接池借取连接执行该操作,执行完毕后将其归还给连接池。

如果你在 Vert.x 环境中运行,可以将你的 Vertx 实例传递给它:

PgConnectOptions connectOptions = new PgConnectOptions()
  .setPort(5432)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret");

// 连接池选项
PoolOptions poolOptions = new PoolOptions()
  .setMaxSize(5);

// 创建池化客户端
SqlClient client = PgBuilder
  .client()
  .with(poolOptions)
  .connectingTo(connectOptions)
  .using(vertx)
  .build();

当你不再需要该客户端时,应当将其释放关闭:

client.close();

当你需要在同一连接上连续执行多个操作时,需要从连接池中获取一个 连接(connection)。

你可以很方便地从池中获取连接:

PgConnectOptions connectOptions = new PgConnectOptions()
  .setPort(5432)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret");

// 连接池选项
PoolOptions poolOptions = new PoolOptions()
  .setMaxSize(5);

// 创建池化客户端
Pool pool = PgBuilder
  .pool()
  .with(poolOptions)
  .connectingTo(connectOptions)
  .using(vertx)
  .build();

// 从连接池获取连接
pool.getConnection().compose(conn -> {
  System.out.println("Got a connection from the pool");

  // 所有操作均在同一连接上执行
  return conn
    .query("SELECT * FROM users WHERE id='julien'")
    .execute()
    .compose(res -> conn
      .query("SELECT * FROM users WHERE id='emad'")
      .execute())
    .onComplete(ar -> {
      // 将连接释放回连接池
      conn.close();
    });
}).onComplete(ar -> {
  if (ar.succeeded()) {
    System.out.println("Done");
  } else {
    System.out.println("Something went wrong " + ar.cause().getMessage());
  }
});

连接使用完毕后,你必须将其关闭以归还给连接池,以便连接能够被复用。

命令流水线(Command pipelining)

在某些使用场景下,命令流水线能够显著提高数据库访问性能。

你可以配置客户端启用流水线模式:

Pool pool = PgBuilder
  .pool()
  .connectingTo(connectOptions.setPipeliningLimit(16))
  .with(poolOptions)
  .using(vertx)
  .build();

默认的流水线限制(pipelining limit)为 256。

你可以将该值设置为 1 以禁用流水线。

连接池 vs 池化客户端(Pool versus pooled client)

PgBuilder 允许你创建连接池(pool)或池化客户端(pooled client):

SqlClient client = PgBuilder
  .client()
  .with(poolOptions)
  .connectingTo(connectOptions)
  .using(vertx)
  .build();

// 流水线式执行
Future<RowSet<Row>> res1 = client.query(sql).execute();

// 连接池
Pool pool = PgBuilder
  .pool()
  .connectingTo(connectOptions)
  .with(poolOptions)
  .using(vertx)
  .build();

// 非流水线式执行
Future<RowSet<Row>> res2 = pool.query(sql).execute();
  • 连接池(pool)操作默认不采用流水线式,只有从连接池借出的连接才会进行流水线式处理

  • 池化客户端(pooled client)操作采用流水线式,但你无法直接从池化客户端中借取连接

连接池共享(Pool sharing)

你可以在多个 Verticle 或同一 Verticle 的多个实例之间共享同一个连接池。此类连接池应在 Verticle 外部创建,否则当创建它的 Verticle 被卸载时,连接池也会被随之关闭:

Pool pool = Pool.pool(database, new PoolOptions().setMaxSize(maxSize));
vertx.deployVerticle(() -> new VerticleBase() {
  @Override
  public Future<?> start() throws Exception {
    // 使用连接池
    return super.start();
  }
}, new DeploymentOptions().setInstances(4));

你也可以直接在各个 Verticle 内部声明获取共享连接池:

vertx.deployVerticle(() -> new VerticleBase() {
  Pool pool;
  @Override
  public Future<?> start() throws Exception {
    // 获取或创建共享连接池
    // 本质上是获取对该池的一份租约(Lease)
    // 当该 Verticle 卸载时,租约会自动释放
    pool = PgBuilder.pool()
      .with(new PoolOptions()
        .setMaxSize(maxSize)
        .setShared(true)
        .setName("my-pool"))
      .connectingTo(database)
      .using(vertx)
      .build();
    return super.start();
  }
}, new DeploymentOptions().setInstances(4));

首次创建共享连接池时,系统会为该连接池分配底层资源。随后的调用将直接复用该池并创建新的租约。当所有租约均被关闭后,底层系统资源才会被完全释放。

默认情况下,当连接池需要创建 TCP 物理连接时,会复用当前的 EventLoop。因此共享连接池会随机使用正在使用它的各 Verticle 的 EventLoop。

你也可以独立于调用上下文,为连接池显式分配指定数量的 EventLoop:

Pool pool = PgBuilder.pool()
  .with(new PoolOptions()
    .setMaxSize(maxSize)
    .setShared(true)
    .setName("my-pool")
    .setEventLoopSize(4))
  .connectingTo(database)
  .using(vertx)
  .build();

Unix Domain Socket

有时出于易用性、安全性或性能方面的考虑,需要通过 Unix Domain Socket 进行连接。

为此,请在 PgConnectOptions#setHost 中指定 domain socket 的文件路径:

PgConnectOptions connectOptions = new PgConnectOptions()
  .setHost("/var/run/postgresql")
  .setPort(5432)
  .setDatabase("the-db");

// 连接池选项
PoolOptions poolOptions = new PoolOptions()
  .setMaxSize(5);

// 创建池化客户端
Pool client = PgBuilder
  .pool()
  .connectingTo(connectOptions)
  .with(poolOptions)
  .using(vertx)
  .build();
当运行在 JDK 16+ 上,或使用 本地传输(native transport) 时,支持 Unix Domain Socket。

连接重试(Connect retries)

你可以配置客户端在建立连接失败时自动进行重试:

options
  .setReconnectAttempts(2)
  .setReconnectInterval(1000);

配置(Configuration)

客户端为你提供了多种配置方式。

数据对象(data object)

配置客户端的一种简单方式是指定 PgConnectOptions 数据对象。

PgConnectOptions connectOptions = new PgConnectOptions()
  .setPort(5432)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret");

// 连接池选项
PoolOptions poolOptions = new PoolOptions().setMaxSize(5);

// 基于数据对象创建连接池
Pool pool = PgBuilder.pool()
  .with(poolOptions)
  .connectingTo(connectOptions)
  .using(vertx)
  .build();

pool.getConnection()
  .onComplete(ar -> {
    // 处理你的数据库连接
  });

你也可以通过 setProperties 或 addProperty 方法配置通用属性。请注意,setProperties 会覆盖客户端的默认属性。

当将此客户端与 CockroachDB DBaaS 一起使用时,需要通过 addProperty("options", "--cluster=<cluster-id>") 或在 URL 中使用 …​&options=--cluster%3D<cluster-id> 包含 cluster 选项。

例如,你可以通过添加 search_path 属性为连接设置默认 schema:

PgConnectOptions connectOptions = new PgConnectOptions();

// 设置默认 schema
Map<String, String> props = new HashMap<>();
props.put("search_path", "myschema");
connectOptions.setProperties(props);

有关可用属性的更多信息,请参阅 PostgreSQL 官方手册。

连接 URI(Connection URI)

除了使用 PgConnectOptions 数据对象进行配置外,如果你想通过连接 URI 进行配置,我们也为你提供了另一种连接方式:

String connectionUri = "postgresql://dbuser:secretpassword@database.server.com:5432/mydb";

// 基于连接 URI 创建连接池
Pool pool = PgBuilder.pool()
  .connectingTo(connectionUri)
  .using(vertx)
  .build();

// 基于连接 URI 创建连接
PgConnection
  .connect(vertx, connectionUri)
  .onComplete(res -> {
    // 处理你的数据库连接
  });
在连接 URI 中配置的参数将会覆盖默认属性。

有关连接字符串格式的更多信息,请参阅 PostgreSQL 官方手册。

目前,客户端支持以下参数键:

  • host

  • hostaddr

  • port

  • user

  • password

  • dbname

  • sslmode

  • sslnegotiation

  • channel_binding

其他额外参数将被添加至 properties 中。

连接 URI 解析器会将所有属性键转换为小写。

环境变量(environment variables)

你也可以使用环境变量来设置默认连接配置值,这在需要避免硬编码数据库连接信息时非常实用。你可以参阅 官方文档 获取更多详情。支持以下环境变量:

  • PGHOST

  • PGHOSTADDR

  • PGPORT

  • PGDATABASE

  • PGUSER

  • PGPASSWORD

  • PGSSLMODE

  • PGSSLNEGOTIATION

  • PGCHANNELBINDING

如果你在连接时未指定数据对象或连接 URI 字符串,环境变量将具有更高的优先级。

$ PGUSER=user   PGHOST=the-host   PGPASSWORD=secret   PGDATABASE=the-db   PGPORT=5432   PGSSLMODE=DISABLE   PGCHANNELBINDING=prefer
Pool pool = PgBuilder.pool()
  .using(vertx)
  .build();

// 从环境变量创建连接
PgConnection.connect(vertx)
  .onComplete(res -> {
    // 处理你的数据库连接
  });

SASL SCRAM-SHA-256 认证机制

vertx-pg-client 支持 SCRAM-SHA-256 和 SCRAM-SHA-256-PLUS SASL 认证机制。从 v5.0.0 版本开始,vertx-pg-client 自带了 com.ongres.scram 库依赖,从旧版本迁移的用户可以从其 pom.xml 和 build.gradle 文件中移除 com.ongres.scram 库。

如果客户端仅使用其他认证方式,则可以使用 Maven <exclusions> 或 Gradle exclude 排除传递性 com.ongres.scram 依赖,以减小软件打包体积。

执行查询

当你不需要事务或仅运行单次查询时,可以直接在连接池上执行查询;连接池会自动从池中借调一个连接来执行该查询并向你返回结果。

以下演示如何执行简单查询:

client
  .query("SELECT * FROM users WHERE id='julien'")
  .execute()
  .onComplete(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> result = ar.result();
    System.out.println("Got " + result.size() + " rows ");
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

预编译查询(Prepared Query)

你同样可以对预编译查询执行类似操作。

SQL 字符串中可以通过位置引用参数,使用数据库特定的占位符语法 `$1`, `$2`, etc…​:

client
  .preparedQuery("SELECT * FROM users WHERE id=$1")
  .execute(Tuple.of("julien"))
  .onComplete(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> rows = ar.result();
    System.out.println("Got " + rows.size() + " rows ");
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

查询方法返回异步的 RowSet 实例,适用于各类 SELECT 查询:

client
  .preparedQuery("SELECT first_name, last_name FROM users")
  .execute()
  .onComplete(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> rows = ar.result();
    for (Row row : rows) {
      System.out.println("User " + row.getString(0) + " " + row.getString(1));
    }
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

对于数据变更操作,同样支持获取受影响的行数:

client
  .preparedQuery("INSERT INTO users (first_name, last_name) VALUES ($1, $2)")
  .execute(Tuple.of("Julien", "Viet"))
  .onComplete(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> rows = ar.result();
    System.out.println(rows.rowCount());
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

Row 允许通过索引序号或列名访问数据字段:

System.out.println("User " + row.getString(0) + " " + row.getString(1));
System.out.println("User " + row.getString("first_name") + " " + row.getString("last_name"));

客户端不会对参数类型进行任何魔法猜测,所有的参数都应当严格遵守 SQL 类型系统。

你可以执行批处理预编译查询:

List<Tuple> batch = new ArrayList<>();
batch.add(Tuple.of("julien", "Julien Viet"));
batch.add(Tuple.of("emad", "Emad Alblueshi"));

// 执行批处理
client
  .preparedQuery("INSERT INTO USERS (id, name) VALUES ($1, $2)")
  .executeBatch(batch)
  .onComplete(res -> {
  if (res.succeeded()) {

    // 获取所有行结果
    RowSet<Row> rows = res.result();
  } else {
    System.out.println("Batch failed " + res.cause());
  }
});

RETURNING 子句

你可以在查询中使用 'RETURNING' 子句获取自动生成的主键:

client
  .preparedQuery("INSERT INTO color (color_name) VALUES ($1), ($2), ($3) RETURNING color_id")
  .execute(Tuple.of("white", "red", "blue"))
  .onSuccess(rows -> {
    for (Row row : rows) {
      System.out.println("generated key: " + row.getInteger("color_id"));
    }
});

只要 SQL 中包含 RETURNING 子句,此方式对任何 SQL 均有效:

client
  .query("DELETE FROM color RETURNING color_name")
  .execute()
  .onSuccess(rows -> {
    for (Row row : rows) {
      System.out.println("deleted color: " + row.getString("color_name"));
    }
  });

带有 RETURNING 子句的批量查询会生成一个 RowSet,其中为批处理中的每个元素包含一行:

client
  .preparedQuery("INSERT INTO color (color_name) VALUES ($1) RETURNING color_id")
  .executeBatch(Arrays.asList(Tuple.of("white"), Tuple.of("red"), Tuple.of("blue")))
  .onSuccess(res -> {
    for (RowSet<Row> rows = res; rows != null; rows = rows.next()) {
      Integer colorId = rows.iterator().next().getInteger("color_id");
      System.out.println("generated key: " + colorId);
    }
  });

使用独立连接

获取数据库连接

当你需要执行一系列顺序查询(不包含事务)时,可以创建新连接或直接从连接池借取连接。请牢记,在从池中获取连接到归还连接期间,你需要妥善维护该连接,因为它可能因服务器端空闲超时等原因被意外关闭:

pool
  .getConnection()
  .compose(connection ->
    connection
      .preparedQuery("INSERT INTO Users (first_name,last_name) VALUES ($1, $2)")
      .executeBatch(Arrays.asList(
        Tuple.of("Julien", "Viet"),
        Tuple.of("Emad", "Alblueshi")
      ))
      .compose(res -> connection
        // 处理返回的行数据
        .query("SELECT COUNT(*) FROM Users")
        .execute()
        .map(rows -> rows.iterator().next().getInteger(0)))
      // 将连接安全归还给连接池
      .eventually(() -> connection.close())
  ).onSuccess(count -> {
  System.out.println("Insert users, now the number of users is " + count);
});

同样可以创建并使用预编译查询(Prepared Query):

connection
  .prepare("SELECT * FROM users WHERE first_name LIKE $1")
  .compose(pq ->
    pq.query()
      .execute(Tuple.of("Julien"))
      .eventually(() -> pq.close())
  ).onSuccess(rows -> {
  // 获取所有行数据
});

简化的连接管理 API

在使用连接池时,可以调用 withConnection 方法,传入一个在连接作用域内执行的函数。

该方法会自动从连接池借出连接,并将该连接作为参数调用你的函数。

传入的函数必须返回一个包含任意结果的 Future。

在该 Future 完成后,连接会被自动安全归还给连接池,并对外返回最终的处理结果:

pool.withConnection(connection ->
  connection
    .preparedQuery("INSERT INTO Users (first_name,last_name) VALUES ($1, $2)")
    .executeBatch(Arrays.asList(
      Tuple.of("Julien", "Viet"),
      Tuple.of("Emad", "Alblueshi")
    ))
    .compose(res -> connection
      // 处理查询返回的行数据
      .query("SELECT COUNT(*) FROM Users")
      .execute()
      .map(rows -> rows.iterator().next().getInteger(0)))
).onSuccess(count -> {
  System.out.println("Insert users, now the number of users is " + count);
});

使用事务

基于连接的事务管理

你可以使用 SQL BEGIN/COMMIT/ROLLBACK 语句手动执行事务;若这么做,你必须获取并自行管理 SqlConnection 连接。

或者,推荐使用 SqlConnection 提供的事务 API:

pool.getConnection()
  // 事务必须在独占的物理连接中运行
  .onSuccess(conn -> {
    // 开启事务
    conn.begin()
      .compose(tx -> conn
        // 执行一系列操作
        .query("INSERT INTO Users (first_name,last_name) VALUES ('Julien','Viet')")
        .execute()
        .compose(res2 -> conn
          .query("INSERT INTO Users (first_name,last_name) VALUES ('Emad','Alblueshi')")
          .execute())
        // 提交事务
        .compose(res3 -> tx.commit()))
      // 无论成功还是失败,均将连接归还至连接池
      .eventually(() -> conn.close())
      .onSuccess(v -> System.out.println("Transaction succeeded"))
      .onFailure(err -> System.out.println("Transaction failed: " + err.getMessage()));
  });

当数据库服务端判定当前事务失败时(例如出现著名的 current transaction is aborted, commands ignored until end of transaction block),事务会自动回滚,并且 completion Future 会以 TransactionRollbackException 异常宣告失败:

tx.completion()
  .onFailure(err -> {
    System.out.println("Transaction failed => rolled back");
  });

简化的事务管理 API

在使用连接池时,你可以调用 withTransaction,传入一个在事务内执行的函数。

该方法会自动从连接池借取连接、开启事务,并调用你的业务函数,由客户端在该事务的上下文中执行所有数据库操作。

该业务函数必须返回包含任意操作结果的 Future:

  • 当 Future 成功时,客户端会自动提交该事务;

  • 当 Future 失败时,客户端会自动回滚该事务。

事务执行完毕(无论提交或回滚)后,连接都会自动归还给连接池,并向调用方返回整体操作的最终结果:

pool.withTransaction(client -> client
  .query("INSERT INTO Users (first_name,last_name) VALUES ('Julien','Viet')")
  .execute()
  .flatMap(res -> client
    .query("INSERT INTO Users (first_name,last_name) VALUES ('Emad','Alblueshi')")
    .execute()
    // 转换为业务提示消息
    .map("Users inserted")))
  .onSuccess(v -> System.out.println("Transaction succeeded"))
  .onFailure(err -> System.out.println("Transaction failed: " + err.getMessage()));

游标(Cursor)与流式查询

默认情况下,执行预编译查询会一次性抓取所有数据行。你可以使用 Cursor 来精准控制每次读取的数据行数:

connection
  .prepare("SELECT * FROM users WHERE first_name LIKE $1")
  .onComplete(ar0 -> {
  if (ar0.succeeded()) {
    PreparedStatement pq = ar0.result();

    // 游标要求在事务内部运行
    connection
      .begin()
      .onComplete(ar1 -> {
      if (ar1.succeeded()) {
        Transaction tx = ar1.result();

        // 创建游标
        Cursor cursor = pq.cursor(Tuple.of("julien"));

        // 读取 50 行
        cursor.read(50).onComplete(ar2 -> {
          if (ar2.succeeded()) {
            RowSet<Row> rows = ar2.result();

            // 检查是否还有剩余数据行?
            if (cursor.hasMore()) {
              // 重复读取流程...
            } else {
              // 全部数据行读取完毕 - 提交事务
              tx.commit();
            }
          }
        });
      }
    });
  }
});

当游标需要提前释放时,必须显式调用关闭方法:

cursor
  .read(50)
  .onComplete(ar2 -> {
  if (ar2.succeeded()) {
    // 关闭游标
    cursor.close();
  }
});

游标还提供了更方便的流式 API,尤其是在搭配 RxJava 版本时极为顺畅:

connection
  .prepare("SELECT * FROM users WHERE first_name LIKE $1")
  .onComplete(ar0 -> {
  if (ar0.succeeded()) {
    PreparedStatement ps = ar0.result();

    // 数据流要求在事务内部运行
    connection
      .begin()
      .onComplete(ar1 -> {
      if (ar1.succeeded()) {
        Transaction tx = ar1.result();

        // 每次抓取 50 行
        RowStream<Row> stream = ps.createStream(50, Tuple.of("julien"));

        // 使用数据流
        stream.exceptionHandler(err -> {
          System.out.println("Error: " + err.getMessage());
          ps.close();
        });
        stream.endHandler(v -> {
          // 关闭流以释放数据库中的游标资源
          stream
            .close()
            .onComplete(closed -> {
            tx.commit()
              .onComplete(committed -> {
                System.out.println("End of stream");
                ps.close();
            });
          });
        });
        stream.handler(row -> {
          System.out.println("User: " + row.getString("last_name"));
        });
      }
    });
  }
});

数据流会以每次 50 行的批次读取并推送;当该批数据行全部传递给处理器后,再读取新的 50 行批次,以此类推。

该流支持随时暂停(Pause)或恢复(Resume);已加载的行将缓存在内存中直到被消费,在此期间游标会暂停迭代抓取。

PostgreSQL 会在事务结束时销毁游标,因此游标 API 必须在事务内部使用,否则你很可能会收到 PostgreSQL 34000 错误。

查询链路追踪(Tracing queries)

当 Vert.x 启用了链路追踪(Tracing)时,SQL 客户端可以对查询执行过程进行全链路追踪。

客户端会上报以下 客户端(Client) Span:

  • 操作名称为 QueryMessage

  • 包含以下 Tags:

  • db.system:数据库管理系统产品名称

  • db.user:数据库用户名

  • db.instance:数据库实例

  • db.statement:SQL 查询语句

  • db.type:固定为 sql

默认的链路追踪策略为 PROPAGATE,客户端仅在参与现有活跃链路时才会创建 Span。

你可以通过 setTracingPolicy 更改客户端追踪策略,例如设置为 ALWAYS 以始终上报 Span:

options.setTracingPolicy(TracingPolicy.ALWAYS);

PostgreSQL 数据类型映射(PostgreSQL type mapping)

目前客户端支持以下 PostgreSQL 类型:

PostgreSQL 类型 Java 类型 Java 数组类型 说明

BOOLEAN

java.lang.Boolean

java.lang.Boolean[]

INT2

java.lang.Short

java.lang.Short[]

INT4

java.lang.Integer

java.lang.Integer[]

INT8

java.lang.Long

java.lang.Long[]

FLOAT4

java.lang.Float

java.lang.Float[]

FLOAT8

java.lang.Double

java.lang.Double[]

CHAR

java.lang.String

java.lang.Character[]

VARCHAR

java.lang.String

java.lang.String[]

TEXT

java.lang.String

java.lang.String[]

ENUM

java.lang.String

java.lang.String[]

NAME

java.lang.String

java.lang.String[]

SERIAL2

java.lang.Short

-

SERIAL4

java.lang.Integer

-

SERIAL8

java.lang.Long

-

NUMERIC

io.vertx.sqlclient.data.Numeric

io.vertx.sqlclient.data.Numeric[]

UUID

java.util.UUID

java.util.UUID[]

DATE

java.time.LocalDate

java.time.LocalDate[]

TIME

java.time.LocalTime

java.time.LocalTime[]

TIMETZ

java.time.OffsetTime

java.time.OffsetTime[]

TIMESTAMP

java.time.LocalDateTime

java.time.LocalDateTime[]

TIMESTAMPTZ

java.time.OffsetDateTime

java.time.OffsetDateTime[]

INTERVAL

io.vertx.pgclient.data.Interval

io.vertx.pgclient.data.Interval[]

BYTEA

io.vertx.core.buffer.Buffer

io.vertx.core.buffer.Buffer[]

JSON

java.lang.Object

java.lang.Object[]

参见 [handling-json]

JSONB

java.lang.Object

java.lang.Object[]

参见 [handling-json]

POINT

io.vertx.pgclient.data.Point

io.vertx.pgclient.data.Point[]

LINE

io.vertx.pgclient.data.Line

io.vertx.pgclient.data.Line[]

LSEG

io.vertx.pgclient.data.LineSegment

io.vertx.pgclient.data.LineSegment[]

BOX

io.vertx.pgclient.data.Box

io.vertx.pgclient.data.Box[]

PATH

io.vertx.pgclient.data.Path

io.vertx.pgclient.data.Path[]

POLYGON

io.vertx.pgclient.data.Polygon

io.vertx.pgclient.data.Polygon[]

CIRCLE

io.vertx.pgclient.data.Circle

io.vertx.pgclient.data.Circle[]

TSVECTOR

java.lang.String

java.lang.String[]

TSQUERY

java.lang.String

java.lang.String[]

INET

io.vertx.pgclient.data.Inet

io.vertx.pgclient.data.Inet[]

CIDR

io.vertx.pgclient.data.Cidr

io.vertx.pgclient.data.Cidr[]

MONEY

io.vertx.pgclient.data.Money

io.vertx.pgclient.data.Money[]

UNKNOWN

java.lang.String

java.lang.String[]

Tuple 解码在存储数值时会使用上述类型。在可能的情况下,它还会对实际值执行动态类型转换:

pool
  .query("SELECT 1::BIGINT "VAL"")
  .execute()
  .onComplete(ar -> {
    RowSet<Row> rowSet = ar.result();
    Row row = rowSet.iterator().next();

    // 存储为 java.lang.Long
    Object value = row.getValue(0);

    // 转换为 java.lang.Integer
    Integer intValue = row.getInteger(0);
  });

Tuple 编码在进行值编码时会采用上述类型映射,除非该类型为数值型,此时将改用 java.lang.Number:

pool
  .query("SELECT 1::BIGINT "VAL"")
  .execute()
  .onComplete(ar -> {
    RowSet<Row> rowSet = ar.result();
    Row row = rowSet.iterator().next();

    // 存储为 java.lang.Long
    Object value = row.getValue(0);

    // 转换为 java.lang.Integer
    Integer intValue = row.getInteger(0);
  });

处理 JSON

PostgreSQL JSON 和 JSONB 类型由以下 Java 类型表示:

  • String

  • Number

  • Boolean

  • io.vertx.core.json.JsonObject

  • io.vertx.core.json.JsonArray

  • io.vertx.sqlclient.Tuple#JSON_NULL 用于表示 JSON null 字面量

Tuple tuple = Tuple.of(
  Tuple.JSON_NULL,
  new JsonObject().put("foo", "bar"),
  3);

// 读取 JSON
Object value = tuple.getValue(0); // 预期为 JSON_NULL

//
value = tuple.get(JsonObject.class, 1); // 预期为 JsonObject

//
value = tuple.get(Integer.class, 2); // 预期为 3
value = tuple.getInteger(2); // 预期为 3

处理 NUMERIC

Numeric Java 类型用于表示 PostgreSQL 的 NUMERIC 类型。

Numeric numeric = row.get(Numeric.class, 0);
if (numeric.isNaN()) {
  // 处理 NaN
} else {
  BigDecimal value = numeric.bigDecimalValue();
}

暂不支持的类型

以下类型当前暂不支持:

类型 说明

BIT

尚未实现

VARBIT

尚未实现

MACADDR

尚未实现

MACADDR8

尚未实现

XML

尚未实现

HSTORE

尚未实现

OID

尚未实现

VOID

尚未实现

处理数组(Handling arrays)

数组可在 Tuple 和 Row 上直接操作:

Tuple tuple = Tuple.of(new String[]{ "a", "tuple", "with", "arrays" });

// 向元组添加字符串数组
tuple.addArrayOfString(new String[]{"another", "array"});

// 获取第一个字符串数组
String[] array = tuple.getArrayOfStrings(0);

处理日期/时间无穷大(Handling Date/Time infinity)

PostgreSQL 定义了特殊值来表示正负无穷大(infinity)。

对应类型的 max/min 常量表示该 特殊值:

  • OffsetDateTime.MAX/OffsetDateTime.MIN

  • LocalDateTime.MAX/LocalDateTime.MIN

  • LocalDate.MAX/LocalDate.MIN

client
  .query("SELECT 'infinity'::DATE "LocalDate"")
  .execute()
  .onComplete(ar -> {
    if (ar.succeeded()) {
      Row row = ar.result().iterator().next();
      System.out.println(row.getLocalDate("LocalDate").equals(LocalDate.MAX));
    } else {
      System.out.println("Failure: " + ar.cause().getMessage());
    }
  });

处理自定义类型(Handling custom types)

字符串用于表示自定义类型,无论是发送给 Postgres 还是从 Postgres 返回。

你可以从 PostgreSQL 中读取并以字符串形式获取自定义类型:

client
  .preparedQuery("SELECT address, (address).city FROM address_book WHERE id=$1")
  .execute(Tuple.of(3))
  .onComplete(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> rows = ar.result();
    for (Row row : rows) {
      System.out.println("Full Address " + row.getString(0) + ", City " + row.getString(1));
    }
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

你也可以通过提供字符串向 PostgreSQL 写入自定义类型:

client
  .preparedQuery("INSERT INTO address_book (id, address) VALUES ($1, $2)")
  .execute(Tuple.of(3, "('Anytown', 'Second Ave', false)"))
  .onComplete(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> rows = ar.result();
    System.out.println(rows.rowCount());
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

全文搜索使用 Java String 进行处理:

client
  .preparedQuery("SELECT to_tsvector( $1 ) @@ to_tsquery( $2 )")
  .execute(Tuple.of("fat cats ate fat rats", "fat & rat"))
  .onComplete(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> rows = ar.result();
    for (Row row : rows) {
      System.out.println("Match : " + row.getBoolean(0));
    }
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

tsvector 和 tsquery 可以使用 Java String 从数据库中提取:

client
  .preparedQuery("SELECT to_tsvector( $1 ), to_tsquery( $2 )")
  .execute(Tuple.of("fat cats ate fat rats", "fat & rat"))
  .onComplete(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> rows = ar.result();
    for (Row row : rows) {
      System.out.println("Vector : " + row.getString(0) + ", query : "+row.getString(1));
    }
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

处理枚举类型(Handling enumerated types)

PostgreSQL 枚举类型 会映射为 Java 字符串。

client
  .preparedQuery("INSERT INTO colors VALUES ($2)")
  .execute(Tuple.of("red"))
  .onComplete(res -> {
    // ...
  });

使用 Java 枚举类型(Using Java enum types)

你可以将 Java 枚举类型 映射到以下数据库列类型:

  • 字符串(VARCHAR、TEXT)

  • PostgreSQL 枚举类型

  • 数值(INT2、INT4、INT8)

client
  .preparedQuery("INSERT INTO colors VALUES ($1)")
  .execute(Tuple.of(Color.red))
  .flatMap(res ->
    client
      .preparedQuery("SELECT color FROM colors")
      .execute()
  ).onComplete(res -> {
    if (res.succeeded()) {
      RowSet<Row> rows = res.result();
      for (Row row : rows) {
        System.out.println(row.get(Color.class, "color"));
      }
    }
});

字符串和 PostgreSQL 枚举类型会与 Java 枚举通过 name() 方法返回的名称进行匹配。

数值类型会与 Java 枚举通过 ordinal() 方法返回的序号进行匹配。

使用收集器查询(Collector queries)

你可以在查询 API 中结合使用 Java Collector 收集器:

Collector<Row, ?, Map<Long, String>> collector = Collectors.toMap(
  row -> row.getLong("id"),
  row -> row.getString("last_name"));

// 结合收集器执行查询
client.query("SELECT * FROM users")
  .collecting(collector)
  .execute()
  .onComplete(ar -> {
  if (ar.succeeded()) {
    SqlResult<Map<Long, String>> result = ar.result();

    // 获取收集器生成的 Map
    Map<Long, String> map = result.value();
    System.out.println("Got " + map);
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

收集器在处理过程中绝不能保留对 Row 对象的引用,因为在处理整个结果集时内部复用的是单一行实例。

Java Collectors 提供了许多开箱即用的预定义收集器,例如你可以轻松地直接从行结果集中构建字符串:

Collector<Row, ?, String> collector = Collectors.mapping(
  row -> row.getString("last_name"),
  Collectors.joining(",", "(", ")")
);

// 结合收集器执行查询
client
  .query("SELECT * FROM users")
  .collecting(collector)
  .execute()
  .onComplete(ar -> {
    if (ar.succeeded()) {
      SqlResult<String> result = ar.result();

      // 获取收集器生成的字符串
      String list = result.value();
      System.out.println("Got " + list);
    } else {
      System.out.println("Failure: " + ar.cause().getMessage());
    }
  });

发布 / 订阅(Pub/sub)

PostgreSQL 支持发布/订阅(Pub/sub)通信通道。

你可以设置 notificationHandler 来接收 PostgreSQL 的通知(notifications):

connection.notificationHandler(notification -> {
  System.out.println("Received " + notification.getPayload() + " on channel " + notification.getChannel());
});

connection
  .query("LISTEN some-channel")
  .execute()
  .onComplete(ar -> {
  System.out.println("Subscribed to channel");
});

PgSubscriber 是一个通道管理器,它通过管理单个连接来提供按通道粒度的订阅功能:

PgSubscriber subscriber = PgSubscriber.subscriber(vertx, new PgConnectOptions()
  .setPort(5432)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret")
);

// 你可以在连接前设置订阅通道
subscriber.channel("channel1").handler(payload -> {
  System.out.println("Received " + payload);
});

subscriber
  .connect()
  .onComplete(ar -> {
  if (ar.succeeded()) {

    // 或者在连接成功后设置订阅通道
    subscriber.channel("channel2").handler(payload -> {
      System.out.println("Received " + payload);
    });
  }
});

传递给 channel 方法的通道名称将作为 PostgreSQL 发送通知时使用的通道精确名称。请注意,这与 SQL 中的通道名称表示不同,在底层 PgSubscriber 会将提交的通道名预处理为带引号的标识符:

PgSubscriber subscriber = PgSubscriber.subscriber(vertx, new PgConnectOptions()
  .setPort(5432)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret")
);

subscriber
  .connect()
  .onComplete(ar -> {
    if (ar.succeeded()) {
      // 复合通道名称 - 在 PostgreSQL 中需要用双引号标识符
      subscriber.channel("Complex.Channel.Name").handler(payload -> {
        System.out.println("Received " + payload);
      });
      subscriber.channel("Complex.Channel.Name").subscribeHandler(subscribed -> {
        subscriber.actualConnection()
          .query("NOTIFY "Complex.Channel.Name", 'msg'")
          .execute()
          .onComplete(notified -> {
            System.out.println("Notified "Complex.Channel.Name"");
          });
      });

      // PostgreSQL 简单标识符会被强制转换为小写
      subscriber.channel("simple_channel").handler(payload -> {
          System.out.println("Received " + payload);
      });
      subscriber.channel("simple_channel").subscribeHandler(subscribed -> {
        // 以下简单通道标识符会被强制转换为小写
        subscriber.actualConnection()
          .query("NOTIFY Simple_CHANNEL, 'msg'")
          .execute()
          .onComplete(notified -> {
            System.out.println("Notified simple_channel");
          });
      });

      // 以下通道名称超过了当前的限制
      // (NAMEDATALEN = 64) - 1 == 63 个字符,超出部分将被截断
      subscriber.channel("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbb")
        .handler(payload -> {
        System.out.println("Received " + payload);
      });
    }
  });

你可以提供一个重连策略函数,该函数接收重试次数 retries 作为参数,并返回重试等待时间 amountOfTime:

  • 当 amountOfTime < 0 时:订阅者关闭且不再进行重试

  • 当 amountOfTime = 0 时:订阅者立即尝试重新连接

  • 当 amountOfTime > 0 时:订阅者等待 amountOfTime 毫秒后尝试重新连接

PgSubscriber subscriber = PgSubscriber.subscriber(vertx, new PgConnectOptions()
  .setPort(5432)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret")
);

// 最多重试 10 次,每次间隔 100 ms
subscriber.reconnectPolicy(retries -> {
  if (retries < 10) {
    return 100L;
  } else {
    return -1L;
  }
});

默认策略是不进行重连。

通知消息(Notice messages)

PostgreSQL 可以在连接生命周期内发送通知(notice)消息。

默认情况下,此类消息会在控制台中以 warn 级别记录。

你可以在连接上设置一个处理器来捕获这些消息并进行自定义处理:

connection.noticeHandler(notice -> {
  System.out.println("Received notice " + notice.getSeverity() + "" + notice.getMessage());
});

取消正在执行的请求(Cancelling Request)

PostgreSQL 支持取消正在执行中的请求。你可以使用 cancelRequest 取消传输中的请求。取消请求时会向服务器建立一个新连接并发出取消指令,随后关闭该新连接。

connection
  .query("SELECT pg_sleep(20)")
  .execute()
  .onComplete(ar -> {
  if (ar.succeeded()) {
    // 假设这是一个长时间运行的查询并且仍在执行
    System.out.println("Query success");
  } else {
    // 服务器在收到取消请求后将中止当前查询
    System.out.println("Failed to query due to " + ar.cause().getMessage());
  }
});
connection
  .cancelRequest()
  .onComplete(ar -> {
  if (ar.succeeded()) {
    System.out.println("Cancelling request has been sent");
  } else {
    System.out.println("Failed to send cancelling request");
  }
});

取消信号可能会生效,也可能不会产生任何影响 —— 例如,如果信号在服务端已经处理完查询之后才到达,则不会产生任何效果。若取消成功,当前命令将提前终止并返回错误信息。

更多信息请参阅 PostgreSQL 官方文档。

使用 SSL/TLS

若要配置客户端使用 SSL 连接,你可以像配置 Vert.x NetClient 一样配置 PgConnectOptions。 客户端支持所有 SSL 模式,你可以通过 sslmode 进行配置。客户端默认处于 DISABLE SSL 模式。 ssl 参数仅作为设置 sslmode 的快捷方式:setSsl(true) 等同于 setSslMode(VERIFY_CA),而 setSsl(false) 等同于 setSslMode(DISABLE)。

PgConnectOptions options = new PgConnectOptions()
  .setPort(5432)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret")
  .setSslMode(SslMode.VERIFY_CA)
  .setSslOptions(new ClientSSLOptions().setTrustOptions(new PemTrustOptions().addCertPath("/path/to/cert.pem")));

PgConnection
  .connect(vertx, options)
  .onComplete(res -> {
  if (res.succeeded()) {
    // 已通过 SSL 成功连接
  } else {
    System.out.println("Could not connect " + res.cause());
  }
});

SSL 协商(PostgreSQL 17+)

PostgreSQL 17 引入了直接 SSL 协商(Direct SSL negotiation),允许客户端在不进行传统协议握手的情况下立即建立 TLS 连接。 这减少了一个往返连接延迟,并允许标准 SSL 代理(nginx、haproxy)为 PostgreSQL 连接终结 TLS。

客户端通过 SslNegotiation 支持两种 SSL 协商模式:

  • POSTGRES(默认):传统协商模式,客户端发送 SSL 请求并在建立 TLS 前等待服务端响应

  • DIRECT:直接协商模式,立即建立 TLS 连接(需要 PostgreSQL 17+)

PgConnectOptions options = new PgConnectOptions()
  .setPort(5432)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret")
  .setSslMode(SslMode.REQUIRE)
  .setSslNegotiation(SslNegotiation.DIRECT)
  .setSslOptions(new ClientSSLOptions()
    .setTrustOptions(new PemTrustOptions().addCertPath("/path/to/server.crt")));

PgConnection.connect(vertx, options)
  .onComplete(res -> {
    if (res.succeeded()) {
      System.out.println("Connected with direct SSL negotiation");
    } else {
      System.out.println("Could not connect: " + res.cause().getMessage());
    }
  });

你也可以通过连接 URI 配置 SSL 协商:

String connectionUri = "postgresql://localhost/mydb?sslmode=require&sslnegotiation=direct";
PgConnectOptions options = PgConnectOptions.fromUri(connectionUri);

或者使用环境变量:

export PGSSLNEGOTIATION=direct
直接 SSL 协商需要 PostgreSQL 17 或更高版本。在较旧的 PostgreSQL 版本中尝试使用 DIRECT 模式将导致 SSL 握手失败。

更多信息请参阅 Vert.x 官方文档。

通道绑定(SCRAM 认证)

通道绑定(Channel binding)是一种服务端向客户端进行身份认证的机制。 仅在使用 SCRAM authentication 方法的 PostgreSQL 11 或更高版本服务端上,通过 SSL 连接时才支持该功能。

客户端通过 ChannelBinding 支持三种通道绑定模式:

  • DISABLE:禁止使用通道绑定

  • PREFER(默认):表示客户端在通道绑定可用时会优先选择使用它

  • REQUIRE:表示连接必须强制使用通道绑定

PgConnectOptions options = new PgConnectOptions()
  .setPort(5432)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret")
  .setSslMode(SslMode.REQUIRE)
  .setChannelBinding(ChannelBinding.REQUIRE)
  .setSslOptions(new ClientSSLOptions()
    .setTrustOptions(new PemTrustOptions().addCertPath("/path/to/server.crt")));

PgConnection.connect(vertx, options)
  .onComplete(res -> {
    if (res.succeeded()) {
      System.out.println("Connected with channel binding enforcement");
    } else {
      System.out.println("Could not connect: " + res.cause().getMessage());
    }
  });

你也可以通过连接 URI 配置通道绑定:

String connectionUri = "postgresql://localhost/mydb?sslmode=require&channel_binding=require";
PgConnectOptions options = PgConnectOptions.fromUri(connectionUri);

或者使用环境变量:

export PGCHANNELBINDING=require

使用 4 层代理(Using a level 4 proxy)

你可以配置客户端使用 HTTP/1.x CONNECT、SOCKS4a 或 SOCKS5 4 层代理。

更多信息请参阅 Vert.x 官方文档。

使用 7 层代理(Using a level 7 proxy)

7 层代理可以在连接到实际数据库的多个连接之间进行查询负载均衡。发生这种情况时,客户端可能会因为缺乏会话亲和性(session affinity)而产生异常,并引发类似 ERROR: unnamed prepared statement does not exist (26000) 等意外错误。

支持的代理:

  • 配置为 _pool_mode=transaction 的 PgBouncer。

你可以配置客户端以不同方式与代理进行交互:

connectOptions.setUseLayer7Proxy(true);

在此配置下,预编译语句无法被缓存,因此:

  • 必须禁用预编译语句缓存

  • 显式预编译语句只能在事务范围内有效,这意味着你可以使用游标,但游标所对应的预编译语句必须在事务范围内创建和销毁

高级连接池配置(Advanced pool configuration)

服务器负载均衡

你可以为连接池配置多个服务器地址列表以实现负载均衡,而不是仅指定单一服务器:

Pool pool = PgBuilder.pool()
  .with(options)
  .connectingTo(Arrays.asList(server1, server2, server3))
  .using(vertx)
  .build();

连接池在创建底层物理连接时采用轮询(Round-Robin)算法在不同服务器之间进行分配。

该负载均衡策略发生在连接创建阶段,而非从池中借取(Borrow)连接的阶段。

连接池初始化处理器

你可以使用 withConnectHandler 在物理连接创建完成后、放入连接池之前与其进行交互并执行初始化设置:

builder.withConnectHandler(conn -> {
  conn.query(sql).execute().onSuccess(res -> {
    // 将连接归还给池,准备供应用程序正常借用
    conn.close();
  });
});

一旦你完成对该连接的初始化操作,只需直接调用 close() 将其标记为就绪,以便连接池开始调度使用。

动态连接配置

你可以使用 Java Supplier 函数式接口动态提供连接配置详情,而非传入静态的 SqlConnectOptions 实例。

由于该 Supplier 是异步的,因此可用于实现连接配置的动态变更(例如数据库密码自动轮换):

Pool pool = PgBuilder.pool()
  .with(poolOptions)
  .connectingTo(() -> {
    Future<SqlConnectOptions> connectOptions = retrieveOptions();
    return connectOptions;
  })
  .using(vertx)
  .build();