某些查询不应该一直访问数据库。 例如,当您查询主数据 (例如系统设置,语言,翻译等)时,您可能希望避免一直通过网络发送相同的愚蠢查询(和结果)。 例如:
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;}@Overridepublic MockResult[] execute(MockExecuteContext ctx)throws SQLException {Result<?> result;// Add more sophisticated caching criteriaif (ctx.sql().contains("from language")) {// We're using this very useful new Java 8// API for atomic cache value calculationresult = cache.computeIfAbsent(ctx.sql(),sql -> DSL.using(connection).fetch(ctx.sql(),ctx.bindings()));}// All other queries go to the databaseelse {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连接即可。
翻译自: https://www.javacodegeeks.com/2015/03/hack-up-a-simple-jdbc-resultset-cache-using-jooqs-mockdataprovider.html