某些查询不应该一直访问数据库。 例如,当您查询主数据 (例如系统设置,语言,翻译等)时,您可能希望避免一直通过网络发送相同的愚蠢查询(和结果)。 例如:
SELECT * FROM languages
大多数数据库都维护缓冲区高速缓存以加快这些查询的速度,因此您不必总是打磁盘。 一些数据库为每个游标维护结果集缓存,或者它们的JDBC驱动程序甚至可能直接在驱动程序中实现结果集缓存-例如,Oracle中的一项鲜为人知的功能 :
SELECT /*+ RESULT_CACHE */ * FROM languages
但是您可能没有使用Oracle, 并且由于修补JDBC很麻烦 ,因此您可能不得不在数据访问或服务层上一层或两层实施高速缓存:
class LanguageService {
private Cache cache;
List<Language> getLanguages() {
List<Language> result = cache.get();
if (result == null) {
result = doGetLanguages();
cache.put(result);
}
return result;
}
}
而是在JDBC层中进行
尽管这在每个服务和方法级别上都可以正常工作,但是当您仅查询那些结果的一部分时,它可能很快就会变得乏味。 例如,当您添加其他过滤器时会怎样? 您是否也应该缓存该查询? 您应该在缓存上执行过滤器,还是每个过滤器至少访问数据库一次?
class LanguageService {
private Cache cache;
List<Language> getLanguages() { ... }
List<Language> getLanguages(Country country) {
// Another cache?
// Query the cache only and delegate to
// getLanguages()?
// Or don't cache this at all?
}
}
如果我们有以下形式的缓存,那会不会很好:
Map<String, ResultSet> cache;
…缓存可重复使用的JDBC ResultSets
(或更优:jOOQ Results
),并在每次遇到相同的查询字符串时返回相同的结果。
为此使用jOOQ的MockDataProvider
jOOQ附带了一个MockConnection
,它为您实现了JDBC Connection
API, MockConnection
了所有其他对象,例如PreparedStatement
, ResultSet
等。我们已经在上一篇博客文章中介绍了此有用的工具,用于单元测试 。
但是您也可以“模拟”您的连接以实现缓存! 考虑以下非常简单的MockDataProvider
:
class ResultCache implements MockDataProvider {
final Map<String, Result<?>> cache =
new ConcurrentHashMap<>();
final Connection connection;
ResultCache(Connection connection) {
this.connection = connection;
}
@Override
public MockResult[] execute(MockExecuteContext ctx)
throws SQLException {
Result<?> result;
// Add more sophisticated caching criteria
if (ctx.sql().contains("from language")) {
// We're using this very useful new Java 8
// API for atomic cache value calculation
result = cache.computeIfAbsent(
ctx.sql(),
sql -> DSL.using(connection).fetch(
ctx.sql(),
ctx.bindings()
)
);
}
// All other queries go to the database
else {
result = DSL.using(connection).fetch(
ctx.sql(),
ctx.bindings()
);
}
return new MockResult[] {
new MockResult(result.size(), result)
};
}
}
显然,这是一个非常简单的示例。 真正的缓存将涉及无效性(基于时间,基于更新等)以及更多的选择性缓存条件,而不仅仅是from language
匹配。
但是事实是,使用上述ResultCache
,我们现在可以包装所有JDBC连接,并防止对从语言表中查询的所有查询多次访问数据库! 使用jOOQ API的示例:
DSLContext normal = DSL.using(connection);
DSLContext cached = DSL.using(
new MockConnection(new ResultCache(connection))
);
// This executs a select count(*) from language query
assertEquals(4, cached.fetchCount(LANGUAGE));
assertEquals(4, normal.fetchCount(LANGUAGE));
// Let's add another language (using normal config):
LanguageRecord lang = normal.newRecord(LANGUAGE);
lang.setName("German");
lang.store();
// Checking again on the language table:
assertEquals(4, cached.fetchCount(LANGUAGE));
assertEquals(5, normal.fetchCount(LANGUAGE));
缓存就像一个魅力! 请注意,当前的缓存实现仅基于SQL字符串(应该如此)。 如果仅对SQL字符串进行少量修改,就会遇到另一个高速缓存未命中的情况,查询将返回数据库:
// This query is not the same as the cached one, it
// fetches two count(*) expressions. Thus we go back
// to the database and get the latest result.
assertEquals(5, (int) cached
.select(
count(),
count())
.from(LANGUAGE)
.fetchOne()
.value1());
// This still has the "stale" previous result
assertEquals(4, cached.fetchCount(LANGUAGE));
结论
缓存很难。 很难。 除了并发,命名和一处错误外,它还是软件中最困难的三个问题之一。
本文不建议在JDBC级别实现缓存。 您可能会自己决定,也可能不会。 但是,当您这样做时,就可以看到使用jOOQ实现这种缓存有多么容易。
最好的是,您不必在所有应用程序中都使用jOOQ。 您可以将其仅用于此特定用例(以及用于模拟JDBC ),并继续使用JDBC,MyBatis,Hibernate等,只要使用jOOQ MockConnection修补其他框架的JDBC连接即可。