像 ClickHouse 这样快速、强大且功能完备的数据库需要一个语言客户端生态系统,这些客户端可以在相同的属性上保持一致,以最大限度地发挥其潜力。作为我们最受欢迎的集成之一,ClickHouse Java 客户端通过提供一种从 Java 应用程序与 ClickHouse 数据库交互的无缝且高效的方式,发挥着至关重要的作用。此外,该客户端通过提供熟悉且直观的界面、降低数据库操作的复杂性并促进更高效的开发过程,从而提高了开发人员的生产力。
至今近十年,这个软件包的采用证明了社区成员和 ClickHouse 团队成员贡献的质量,他们的协作努力推动了创新并有机地扩展了其功能集。然而,与任何大型软件包一样,总有一个时刻需要解决关键挑战,以改善整体开发人员体验并为未来的扩展做好准备。
在这篇博文中,我们将介绍 ClickHouse Java 项目的重构工作以及其动机。与此同时,您已经可以在我们的存储库中查看早期 alpha 版本。
我们发现的问题
我们听取了用户的意见,并且知道 V1 的整体开发者体验存在挑战 - 我们清楚地听到了您的声音,甚至我们自己也体验过!
简单操作的过度复杂性。
目前,使用客户端进行简单操作需要过多的样板代码和不直观的代码,如本示例所示。多年来,为了增强灵活性,添加了许多抽象层。然而,我们认识到,并非每个用户都需要或希望理解这些抽象层来执行简单的查询。新的 API 旨在提供直观且不言自明的接口,并具有合理的默认值,使用户能够启动他们的项目,而无需深入研究客户端源代码。
数据插入和检索的复杂性。
使用 Java 客户端插入或检索数据需要彻底了解 ClickHouse 数据格式。Java 客户端支持 JSON 和 RowBinary 数据格式用于插入操作。对于具有严格延迟要求的应用程序,最好使用更紧凑且开销更小的格式,例如 RowBinary。
不幸的是,当前的实现要求用户在其应用程序中管理 RowBinary 的数据序列化和反序列化。尽管客户端提供了一组帮助程序,但编写 RowBinary 和 RowBinaryWithDefault 的过程复杂且容易出错,尤其是在处理嵌套数组和映射等复杂类型时。新版本的客户端通过提供通过解析数据对象生成的序列化器来解决此问题。用户只需注册这些对象类,我们的客户端将管理其余部分。
不安全的底层优化
某些底层优化,例如响应对象重用,会引入潜在的危险副作用,这在您的应用程序中可能是意想不到的。我们现在禁用了这些“优化”,而不是默认启用它们 - 所有查询结果都将是不可变的,并且会被延迟反序列化以减少内存占用。
我们正在讨论未来采用选择加入的方法来改变这一点,允许用户在对内存和 CPU 效率有很高要求时激活它们 - 如果您对此有任何想法,请务必说出来!
新客户端 API 目标
我们希望通过以下方式专注于解决这些问题(并让人们“直接开始使用客户端”)
直观的 API
这是一个巨大的动力,因为现有客户端存在很多复杂性 - 我们有太多的层和太多的抽象,很难知道在哪里使用哪些方法。我们希望简化整个流程,以保持事情顺利进行。
改进文档
文档是许多开发人员的祸根 - 它几乎永远不是您所需要的,并且经常随着底层代码的推进而过时。我们希望确保 API 是自文档化的,以便所需的参数是显而易见的(无需查找列出它们的文档),并且可选参数也更加可见。
这还包括更多的实现示例 - 凭借更小的 API 占用空间,新的示例代码应该有助于人们更快地入门!
清理代码库
我相信你们中的许多人都可以证明,您的代码库存在的时间越长,它积累的历史包袱就越多。在过去的 8 年多里,我们已经弃用了一些客户端代码,但现在是时候最终修剪一些东西(并在其位置弃用新事物)。我们相信从长远来看,这将使贡献更加舒适。
让我们立即尝试一下!
设置
创建客户端对象是以构建器样式完成的。所有设置都通过方法设置,然后在内部进行验证
Client.Builder clientBuilder = new Client.Builder()
.addEndpoint(endpoint)
.setUsername(user)
.setPassword(password)
.compressServerResponse(true)
.setDefaultDatabase(database);
this.client = clientBuilder.build();
插入 POJO
这是您如何使用示例 POJO 和新的序列化 API 插入数据的方法(请注意您应该有 getter)
public class ArticleViewEvent {
private Double postId;
private LocalDateTime viewTime;
private String clientId;
}
在使用插入方法之前,我们需要注册它
client.register(ArticleViewEvent.class, client.getTableSchema(TABLE_NAME));
register
的第二个参数是一个表模式,它可以是精确的表模式或自定义模式。
最后,只需将 ArticleViewEvent
对象集合传递给 insert
方法
ArrayList<ArticleViewEvent> events = … ; // filled collection
client.insert(TABLE_NAME, events, new InsertSettings());
新客户端将选择最有效的格式,并将数据传输到服务器。
从流中插入数据
在某些情况下,数据已经采用 ClickHouse 支持的格式。例如,JSONEachRow。在这种情况下,无需注册任何内容,但数据应作为 InputStream 传递
public void insertData_JSONEachRowFormat(InputStream inputStream) {
try {
InsertResponse response = client.insert(TABLE_NAME,
inputStream,
ClickHouseFormat.JSONEachRow)
.get(3, TimeUnit.SECONDS);
log.info("Insert finished: {} rows written",
response.getWrittenRows());
} catch (Exception e) {
log.error("Failed to write JSONEachRow data", e);
throw new RuntimeException(e);
}
}
读取数据
来自 ClickHouse 的数据可以作为记录集合读取,也可以迭代读取。虽然为了您的方便,我们已经实现了 RowBinary 和 Native 格式读取器,但也可以进行原始数据读取。更多格式读取器也即将推出,用于 CSV 和 TSV 等众所周知的格式。
这是从结果集中获取第一条记录的示例
GenericRecord hostnameRecord = client
.queryAll("SELECT hostname()")
.stream()
.findFirst()
.get();
或者只是一个集合
List<GenericRecord> records = client.queryAll(
"SELECT col1, col2, col3 FROM " + DATASET_TABLE
);
for (GenericRecord record : records) {
record.getString(3); // string column col3
}
如果需要来自响应的更多信息,那么 com.clickhouse.client.api.Client#queryRecords
将会很方便。它返回 com.clickhouse.client.api.query.Records
,这是一个可迭代的对象,并且它具有与底层服务器响应的接口。
Records records = client.queryRecords(
"SELECT col1, col2, col3 FROM " + DATASET_TABLE
).get(3, TimeUnit.SECONDS);
System.out.println("Result rows: " + records.getResultRows());
for (GenericRecord record : records) {
record.getString(3); // string column col3
}
后续步骤
在接下来的几周/几个月里,我们将重构一些底层客户端代码并扩展 v2 代码,因此如果您尝试一下,我们将非常高兴!获取 Java 客户端的 0.6.1+ 版本(包括早期 alpha 版本),开始研究 client-v2 - 没有比人们在实际环境中使用它更好的测试了。
我在哪里可以提问/报告问题?
很高兴您提出这个问题!最好的地方是在我们的 GitHub 存储库中使用“v2-feedback”标签创建一个 issue,但您也可以在社区 slack 上找到我们。如果您有任何问题/意见/疑虑,请务必告知我们 - 我们真的想听取您的意见!
遗留客户端呢?
我们将继续支持旧的 (v1) 客户端,提供安全/错误修复/小幅增强功能,直到 2025 年底,但我们未来的重点将不出所料地是新客户端。
另请参阅