Статьи

Взломать простой кэш ResultSet JDBC с помощью MockDataProvider в jOOQ

Некоторые запросы не должны попадать в базу данных постоянно. Например, когда вы запрашиваете основные данные (такие как системные настройки, языки, переводы и т. Д.), Вы можете избегать постоянной отправки одного и того же глупого запроса (и результатов) по сети. Например:

1
SELECT * FROM languages

Большинство баз данных поддерживают буферные кеши для ускорения этих запросов, поэтому вы не всегда обращаетесь к диску. Некоторые базы данных поддерживают кэши набора результатов на курсор, или их драйверы JDBC могут даже реализовывать кэши набора результатов непосредственно в драйвере – например, малоизвестная функция в Oracle :

1
SELECT /*+ RESULT_CACHE */ * FROM languages

Но вы, возможно, не используете Oracle, и поскольку исправление JDBC является проблемой , вы могли прибегнуть к реализации кэша на один или два уровня на уровне доступа к данным или на уровне обслуживания:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
class LanguageService {
    private Cache cache;
 
    List<Language> getLanguages() {
        List<Language> result = cache.get();
 
        if (result == null) {
            result = doGetLanguages();
            cache.put(result);
        }
 
        return result;
    }
}

Делая это в слое JDBC, вместо

Хотя это может хорошо работать на уровне отдельных служб и методов, оно может быстро стать утомительным, когда вы запрашиваете только часть этих результатов. Например, что происходит, когда вы добавляете дополнительный фильтр? Вы также должны кешировать этот запрос? Следует ли выполнять фильтрацию в кеше или использовать базу данных хотя бы один раз для каждого фильтра?

01
02
03
04
05
06
07
08
09
10
11
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?
    }
}

было бы неплохо, если бы у нас был кеш вида:

1
Map<String, ResultSet> cache;

… Который кэширует повторно используемые JDBC ResultSets (или лучше: jOOQ Results ) и возвращает одинаковые результаты каждый раз, когда встречается идентичная строка запроса.

Для этого используйте MockDataProvider от jOOQ

jOOQ поставляется с MockConnection , который реализует API-интерфейс JDBC Connection для вас, насмехаясь над всеми другими объектами, такими как PreparedStatement , ResultSet и т. д. Мы уже представили этот полезный инструмент для модульного тестирования в предыдущем сообщении в блоге .

Но вы также можете «смоделировать» ваше соединение, чтобы реализовать кеш! Рассмотрим следующий очень простой MockDataProvider :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 и предотвратить ResultCache попадание в базу данных для всех запросов, которые запрашивают из языковой таблицы! Пример использования jOOQ API:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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, вы столкнетесь с еще одним отсутствием кэша и запрос вернется к базе данных:

01
02
03
04
05
06
07
08
09
10
11
12
13
// 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 и т. Д., Пока вы исправляете соединения JDBC другой инфраструктуры с помощью jOOQ MockConnection.