在制作 jOOQ 的早期,我考虑了两种选择,即按索引或名称访问 JDBC 值。出于以下原因,我选择按索引访问内容:ResultSet
支持
并非所有 JDBC 驱动程序实际上都支持按名称访问列。我忘了哪些没有,如果它们仍然没有,因为我在13年内再也没有接触过JDBC API的那部分。但有些人没有,这对我来说已经是一个障碍。
名称的语义
此外,在那些支持列名的列名中,列名有不同的语义,主要是两个,JDBC称之为:
关于上述两个的实现有很多歧义,尽管我认为意图非常明确:
- 列名应该产生列名,而不考虑别名,例如 如果投影表达式是
TITLE
BOOK.TITLE AS X
- 列标签应该产生列的标签(或别名),如果没有可用的别名,则产生名称,例如 如果投影表达式是
X
BOOK.TITLE AS X
因此,名称/标签的这种模糊性已经非常令人困惑和担忧。一般来说,ORM似乎不应该依赖它,尽管在Hibernate的情况下,人们可以争辩说Hibernate控制着大多数SQL的生成,至少是为获取实体而生成的SQL。但是,如果用户编写 HQL 或本机 SQL 查询,我将不愿意依赖名称/标签 - 至少没有先在 中查找内容。ResultSetMetaData
歧义
在 SQL 中,在顶层使用不明确的列名是完全可以的,例如:
SELECT id, id, not_the_id AS id
FROM book
这是完全有效的 SQL。不能将此查询嵌套为不允许多义性的派生表,但在顶层可以。现在,您将如何处理顶层的这些重复标签?您无法确定在按名称访问事物时会得到哪一个。前两个可能相同,但第三个非常不同。SELECT
ID
明确区分列的唯一方法是按索引,这是唯一的:, , .1
2
3
性能
我当时也尝试过表演。我不再有基准测试结果,但很容易快速编写另一个基准测试。在下面的基准测试中,我在 H2 内存实例上运行一个简单的查询,并使用访问内容:ResultSet
结果是惊人的:
Benchmark Mode Cnt Score Error Units
JDBCResultSetBenchmark.indexAccess thrpt 7 1130734.076 ± 9035.404 ops/s
JDBCResultSetBenchmark.nameAccess thrpt 7 600540.553 ± 13217.954 ops/s
尽管基准测试在每次调用时运行整个查询,但按索引的访问速度几乎是其两倍!你可以看看H2的代码,它是开源的。它执行以下操作(版本 2.1.212):
private int getColumnIndex(String columnLabel) {
checkClosed();
if (columnLabel == null) {
throw DbException.getInvalidValueException("columnLabel", null);
}
if (columnCount >= 3) {
// use a hash table if more than 2 columns
if (columnLabelMap == null) {
HashMap<String, Integer> map = new HashMap<>();
// [ ... ]
columnLabelMap = map;
if (preparedStatement != null) {
preparedStatement.setCachedColumnLabelMap(columnLabelMap);
}
}
Integer index = columnLabelMap.get(StringUtils.toUpperEnglish(columnLabel));
if (index == null) {
throw DbException.get(ErrorCode.COLUMN_NOT_FOUND_1, columnLabel);
}
return index + 1;
}
// [ ... ]
所以,有一个带有上层大小写的哈希图,每个查找也执行上层大小写。至少,它将映射缓存在预准备语句中,因此:
- 您可以在每一行上重复使用它
- 您可以在语句的多次执行中重用它(至少这就是我解释代码的方式)
因此,对于非常大的结果集,它可能不再那么重要,但对于较小的结果集,它肯定很重要。
针对 ORM 的结论
像Hibernate或jOOQ这样的ORM可以控制大量的SQL和结果集。它确切地知道哪个列在什么位置,在生成SQL查询时已经完成了这项工作。因此,当结果集从数据库服务器返回时,绝对没有理由进一步依赖列名。每个值都将位于预期位置。
在Hibernate中使用列名一定是一些历史性的事情。这可能也是为什么他们曾经生成这些不那么可读的列别名,以确保每个别名都是不明确的。
这似乎是一个明显的改进,无论在现实世界(非基准)查询中的实际收益如何。即使改进只有2%,它也是值得的,因为它会影响每个基于Hibernate的应用程序的每个查询执行。
下面的基准代码,用于复制
package org.jooq.test.benchmarks.local;
import java.io.*;
import java.sql.*;
import java.util.Properties;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.*;
@Fork(value = 1)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 7, time = 3)
public class JDBCResultSetBenchmark {
@State(Scope.Benchmark)
public static class BenchmarkState {
Connection connection;
@Setup(Level.Trial)
public void setup() throws Exception {
try (InputStream is = BenchmarkState.class.getResourceAsStream("/config.properties")) {
Properties p = new Properties();
p.load(is);
connection = DriverManager.getConnection(
p.getProperty("db.url"),
p.getProperty("db.username"),
p.getProperty("db.password")
);
}
}
@TearDown(Level.Trial)
public void teardown() throws Exception {
connection.close();
}
}
@FunctionalInterface
interface ThrowingConsumer<T> {
void accept(T t) throws SQLException;
}
private void run(BenchmarkState state, ThrowingConsumer<ResultSet> c) throws SQLException {
try (Statement s = state.connection.createStatement();
ResultSet rs = s.executeQuery("select c as c1, c as c2, c as c3, c as c4 from system_range(1, 10) as t(c);")) {
c.accept(rs);
}
}
@Benchmark
public void indexAccess(Blackhole blackhole, BenchmarkState state) throws SQLException {
run(state, rs -> {
while (rs.next()) {
blackhole.consume(rs.getInt(1));
blackhole.consume(rs.getInt(2));
blackhole.consume(rs.getInt(3));
blackhole.consume(rs.getInt(4));
}
});
}
@Benchmark
public void nameAccess(Blackhole blackhole, BenchmarkState state) throws SQLException {
run(state, rs -> {
while (rs.next()) {
blackhole.consume(rs.getInt("C1"));
blackhole.consume(rs.getInt("C2"));
blackhole.consume(rs.getInt("C3"));
blackhole.consume(rs.getInt("C4"));
}
});
}
}