30188


EBean（6.17.3）にて、クエリーキャッシングを行なったとき、  
<http://ebean-orm.github.io/docs/features/l2caching/>  
異なるWHERE句のクエリに対して、キャッシュの値が返される場合があるようです。

いわゆるハッシュ値の衝突が起きているようなのですが、  
あまりにも容易に衝突が起きているようなので、回避方法はないでしょうか？

サンプルはPostgreSQLですが、特にDBMLには依存しないと思います。

[code]

    CREATE TABLE test_table
    (
      id bigserial NOT NULL,
      column_a character varying(10),
      column_b character varying(10),
      CONSTRAINT test_table_pkey PRIMARY KEY (id)
    )
    WITH (
      OIDS=FALSE
    );
    ALTER TABLE test_table
      OWNER TO postgres;
    
[/code]

DBデータは以下です。

[code]

    id;column_a;column_b
    1;"SK08";"320"
    2;"SK09";"310"
    3;"SK01";"201"
    4;"SK11";"101"
    
[/code]

再現コードは以下です。  
Entityの定義

[code]

    import javax.persistence.*;
    
    import com.avaje.ebean.Model;
    
    /**
     * テストテーブル
     */
    @Entity
    @Table(name = "test_table")
    public class TestTable extends Model {
    
        @Id
        @Column(name = "id", columnDefinition = "int8", nullable = false, unique = true)
        public Long id;
    
        @Column(name = "column_a", columnDefinition = "varchar(10)", nullable = true)
        public String columnA;
    
        @Column(name = "column_b", columnDefinition = "varchar(10)", nullable = true)
        public String columnB;
    
    }
    
[/code]

検索部分のコードは以下です。  
list1とlist2, list3とlist4が同じ値を返します。  
（list2の検索時にlist1のキャッシュが返る。list4検索時にlist3のキャッシュが返る）

[code]

    List<TestTable> list1 =
            Ebean.getServer(null)
            .find(TestTable.class)
            .setUseQueryCache(true)
            .where()
            .eq("columnA", "SK08")
            .eq("columnB", "320")
            .findList();
    
    List<TestTable> list2 =
            Ebean.getServer(null)
            .find(TestTable.class)
            .setUseQueryCache(true)
            .where()
            .eq("columnA", "SK09")
            .eq("columnB", "310")
            .findList();
    
    List<TestTable> list3 =
            Ebean.getServer(null)
            .find(TestTable.class)
            .setUseQueryCache(true)
            .where()
            .eq("columnA", "SK01")
            .eq("columnB", "201")
            .findList();
    
    List<TestTable> list4 =
            Ebean.getServer(null)
            .find(TestTable.class)
            .setUseQueryCache(true)
            .where()
            .eq("columnA", "SK11")
            .eq("columnB", "101")
            .findList();
    
[/code]

【追記 2016-11-07 16:50】

根本的には、以下のコードでhash1とhash2が同値になるのが原因です（EBeanのクエリーキャッシュのハッシュ値算出アルゴリズムを元に作成）。

[code]

    int hash1 = 0;
    hash1 = hash1 * 31 + "08".hashCode();
    hash1 = hash1 * 31 + "20".hashCode();
    
    int hash2 = 0;
    hash2 = hash2 * 31 + "09".hashCode();
    hash2 = hash2 * 31 + "10".hashCode();
    
[/code]

String.hashCode()のコア部分は以下のような実装になっているようですので、char
codeと桁ズレの組み合わせで容易にハッシュの衝突が起こるようです。

[code]

    char val[] = value;
    
    for (int i = 0; i < value.length; i++) {
        h = 31 * h + val[i];
    }
    hash = h;
    
[/code]

これは（最もキャッシュが有効に機能するはずの）コードテーブルでは普通に想定できる事態だと思います。（ふたつの文字列カラムでレコードが決定する場合）  
問題が大きいですね･･･。


CC BY-SA 3.0
2016-11-07T01:46:25.740
30171
2016-11-07T11:23:27.603
2016-11-07T09:22:55.127
8647
8647
question
java playframework
EBeanのquery cachingで異なるクエリの結果が返される


結論としては、実用的な回避方法は（私の調べた限りでは）ありませんでした。

ただ、`eq()`メソッドの呼び出し順序を変更すれば、ハッシュの値を変えることができますので、誤ったキャッシュを回避することができます。

Ebean 8.1.1  
MySQL 5.6.22

コードを変更して検証しました。

[code]

        DefaultOrmQuery<TestTable> query1 = (DefaultOrmQuery<TestTable>) Ebean.getServer(null)
                .find(TestTable.class)
                .setUseQueryCache(true)
                .where()
                .eq("columnA", "SK08")
                .eq("columnB", "320")
                .query();
    
        List<TestTable> list1 = query1
                        .findList();
    
        logger.info("query1:" + query1.queryHash().hashCode());
        TestTable t1 = list1.get(0);
        logger.info("list1:" + t1.id + " " + t1.columnA + " " + t1.columnB);
    
        DefaultOrmQuery<TestTable> query2 = (DefaultOrmQuery<TestTable>) Ebean.getServer(null)
                .find(TestTable.class)
                .setUseQueryCache(true)
                .where()
                .eq("columnA", "SK09")
                .eq("columnB", "310")
                .query();
    
        List<TestTable> list2 = query2
                .findList();
    
        logger.info("query2:" + query2.queryHash().hashCode());
        TestTable t2 = list2.get(0);
        logger.info("list2:" + t2.id + " " + t2.columnA + " " + t2.columnB);
    
[/code]

この場合、クエリハッシュのハッシュコードはたしかに同一となり、キャッシュが利用されています。

[code]

    情報: query1:-1961149220
    情報: list1:1 SK08 320
    14:48:20.021 [main] DEBUG org.avaje.ebean.cache.QUERY -    GET TestTable(HashQuery@8b1b3cdc) - hit
    情報: query2:-1961149220
    情報: list2:1 SK08 320
    
[/code]

list2の`eq()`メソッドの順序を入れ替えます。

[code]

    DefaultOrmQuery<TestTable> query2 = (DefaultOrmQuery<TestTable>) Ebean.getServer(null)
            .find(TestTable.class)
            .setUseQueryCache(true)
            .where()
            .eq("columnB", "310")
            .eq("columnA", "SK09")
            .query();
    
[/code]

すると、キャッシュは利用されません。

[code]

    情報: query1:-1961149220
    情報: list1:1 SK08 320
    14:50:11.574 [main] DEBUG org.avaje.ebean.cache.QUERY -    GET TestTable(HashQuery@35de6bfa) - cache miss
    情報: query2:903769082
    情報: list2:2 SK09 310
    
[/code]

なお、エンティティクラスに`@Cache(enableQueryCache=true)`をつけています。

[code]

    @Entity
    @Table(name = "test_table")
    @Cache(enableQueryCache=true)
    public class TestTable extends Model {
    
[/code]


CC BY-SA 3.0
2016-11-07T05:50:54.387
30175
2016-11-07T05:50:54.387
4464
30171
answer


今回の問題はEBean ORMのissue #869として登録され、Version 9.1.1で解決するようです。  
<https://github.com/ebean-orm/ebean/issues/869>

根本的な原因はハッシュキーを生成する際に31を用いた実装になっていたことです。


CC BY-SA 3.0
2016-11-07T11:23:27.603
30188
2016-11-07T11:23:27.603
8647
30171
answer