像 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”标签的问题,您也可以在我们的 社区 Slack 上找到我们。如果您有任何问题/评论/疑虑,请告诉我们 - 我们非常希望听到您的意见!
旧版客户端怎么样?
我们将继续支持旧版(v1)客户端,提供安全/错误修复/次要增强功能,直到 2025 年底,但我们未来的重点将毫无疑问地放在新版客户端上。
另请参阅