社内の開発改善からOSS公開へ:

DBFluteのプラグインを開発したお話

Agenda

  1. 自己紹介
  2. U-NEXTの紹介
  3. DBFluteの紹介
  4. 作ったもの
  5. 開発の道のり
  6. 仕組みや使い方
  7. 現状・今後について
  8. 最後に

Who Am I

  • NAGAHORI Shota (長堀 翔太)
  • Twitter: @nashcft
  • Blog: http://nashcft.hatenablog.com/
  • 株式会社U-NEXT R&D部
    • 2016年 5月 入社
    • サーバサイドエンジニア
    • スクラムマスター見習い
    • 技術基盤開発チーム見習い
    • 初Java

U-NEXT?

U-NEXT?

  • オンデマンド動画配信サービスがメイン
  • 12万本以上の映像コンテンツ、20万冊以上の電子書籍
  • 音楽コンテンツ配信も
  • 複数デバイス・マルチアカウント利用可能
    • Web, mobile (Android, iOS), STB, etc
  • FHD, アダプティブストリーミング配信
  • 各社との提携
    • アニメ放題, Rakuten SHOWTIME (プレミアム見放題), ヤマダビデオ,  NHKオンデマンド, TSUTAYA movie (レオパレス入居者向け), etc.

U-NEXT?

前回のJJUG CCC!

  • U-NEXTサービスリニューアルのお話
    • Java 8 + DBFlute導入
    • アーキテクチャの課題と取り組み
    • DBFluteを用いた開発の様子
  • Slide

今回のお話

  • KVS (Redis), Solrへのデータアクセスを DBFlute like に実装できるDBFlute向けプラグインを開発してOSSとして公開したので仕組みの紹介や公開に至るまでについてのお話をします
  • セッション登録時のタイトルは大分盛った感があります
    • "DBFlute like なライブラリ" ってforkして派生版作ったみたいですね😇

DBFlute?

  • http://dbflute.seasar.org/
  • ORM / DB開発支援ツール
  • インクリメンタルな開発・DB変更に対する強さが売り

ORMとしてのDBFlute

  • "DB変更に強い" ORM
    • DBのメタデータを元にDBアクセスのためのクラス群を自動生成する
  • ConditionBeanを用いてTypesafeにクエリを構築できる
    • DB変更によってDBと実装に食い違いが出たらコンパイルエラーで検知する
    • クラス生成の設定によって区分値の選択をAPIで行得るようにできる
  • 2 way SQLも使える
    • クエリによって使い分ける
  • ひとめでConditionBean

開発支援ツールとしてのDBFlute

  • 開発環境構築のためのタスクの殆どを自動化している
    • ReplaceSchema: DBの (再) 構築、テストデータの整合性検証、データ投入
    • AlterCheck: DBの状態とDDLの差分検知と修正支援
    • SchemaHTML, HistoryHTML: DBの情報や変更履歴などをHTMLで出力
    • LoadDataReverse: DBに登録されているデータをtsvやxlsファイルに
    • etc...
  • ORMを使わず支援機能だけ利用することもできる

その他関連ライブラリ等

  • UTFlute: JUnitにDIやMocking、トランザクション機能を付与する拡張ライブラリ
  • ERFlute: EclipseプラグインのERD作成ツール
    • ERMaster-bのfork (ERMaster-bはERMasterのfork)
  • LastaFlute: DBFluteを組み込んだweb app  framework

Documents, Community, etc

つくったもの

  • KVSFlute
    • 専用のschemaを元に、ストレージとしてのKVSへのアクセスやDBアクセスに対するキャッシュとしてKVSを用いるためのクラスを自動生成する
  • SolrFlute 
    • Solrのschemaを元にSolrへのアクセスを行うクラスを自動生成する

開発の道のり

始まり (2015年)

  • リニューアルに向けてJava/DBFluteの採用を決定
  • Redisでキャッシュや一部のデータストア、全文検索にSolr
  • DBFlute以外もTypesafeに書きたい
  • DBFluteの自動生成機能が他でも使えると便利
  • キャッシュする所を毎回jedisだけで書くのは大変
  • solrjだとtypesafeに書けないのでリスクがある
  • なんとかしたい

サーバ構成 (簡略版)

開発体制

  • リニューアルの一環として行われた
  • "DBFluteチーム" (当時DBFlte関連のサポートをしてくださっていた方々) の皆様に助けていただいた

Wrapperを作る (Redis)

  • Redisへの接続部分はFacadeを作成して共通で使用
    • jedisを隠蔽し、select, insertOrUpdate, deleteでRedisを呼び出せるように
  • キャッシュ対象テーブル毎にCRUDのメソッドを持ったLogicクラスを実装
    • キャッシュを含めた検索処理はここに書く
    • Actionクラスから呼び出すときはキャッシュの有無が意識されない
  • Facadeを利用してcolumn単位のキャッシュも実現
    • CB-Embedded Cacheの前身
  • "キャッシュとしてのRedis"

DBFluteのfreegenを使う (Redis, Solr)

  • "データストアとしてのKVS"
  • "MroongaからSolrへ"
  •  Solrはschema.xmlを、KVSは専用にjsonで記述したschemaを用いて自動生成
  • Solrの仕組みはFessにあったものから流用して拡張
    • Fess: OSSの全文検索システム
    • 当時はSolrを用いていて、DBFluteっぽく実装できる仕組みがあった
    • 現在はElasticsearchに移行していて、ESFluteというES版DBFluteを実装している
    • 本日 #ccc_gh7 で開発者の菅谷さんによるセッションがあります! (宣伝)

リニューアルのリリース

  • schemaを元にEntityクラスとかが自動生成できるようになってTypesafeに実装できるようになった 😃
    • Solrアクセスの仕組みはほぼ現在のSolrFluteと同じ状態
  • (特定テーブルに対して) RDBのアクセスに対してRedisのキャッシュを利用する時にRedisの存在を意識せずに実装できるようになった 😃
  • 各アクセスの仕組みはプロジェクトに結びついた実装になっていた 🙁
  • Redisアクセスの検索条件や並び順の処理は自前実装 🙁

2016年入ってから

  • Redisを使いたいところが増えてきた
  • 他の場所でSolrを使う機会ができた
  • 現場「もっと取り回しの良い感じにしたいよね」
  • 独立した仕組みにして汎用化しよう!

開発体制

  • 年始くらいに技術基盤チーム結成
    • DBFluteチームから @jfluteさん、@p1us2eroさん
    • U-NEXTのエンジニアから1, 2名
    • 夏くらいに長堀が技術基盤チームにjoin
  • @p1us2er0さんとU-NEXTメンバーで開発
    • 長堀はどちらかというと勉強とお手伝い
  • @jfluteさんが定期的にレビューに参加

KVSFlute, SolrFluteプロジェクト

  • リニューアル時に開発したSolr, Redisそれぞれに対するアクセスの仕組みをU-NEXTサービスのコードベースから独立させる
  • DBFluteのプラグインとして使えるような仕組みにする
  • DBFluteのAPIと同じような使用感でデータアクセスを実装できるようにする

KVSFlute

  • データストアとキャッシュの仕組みを1つのプラグインとしてまとめる
  • FacadeをさらにBehaviorとConditionBeanで覆う
    • Logicクラスを自分で実装しなくて済むように
    • key以外の検索条件や並び順の設定を簡単かつDBFlute likeに
  • schema毎に実装されるクラスは自動生成させる
    • Behavior, ConditionBean, Entity, Facade
    • キャッシュの仕組みもこれらを自動生成できるようにschemaを定義

SolrFlute

  • 機能変更などは特になし
  • U-NEXTのコードベースから独立させて諸々をKVSFluteのスタイルに寄せるくらい

DBFlute likeな使用感への追求

  • U-NEXT のサーバサイドエンジニアは全員DBFluteハンズオンに取り組んでいるため習熟度が高い
  • DBFlute的に考えて触れられると馴染みやすそう
  • 思想が統一されていると学習時の負担を軽減してくれる

社内リリース (10月頃)

  • SolrFluteは社内ツール開発に使用
  • U-NEXTサービスにはタイミングの問題で即導入はできず...
    • 直近で必要な箇所はリニューアル時の仕組みで実装

仕組みのお話

SolrFlute

  • 生成するもの
  • 自動生成の手順
  • データ操作について

SolrFlute が生成するもの

  • Solrに対するデータアクセスの仕組み
    • DBFlute likeに実装するためのwrapper
      • Solrへの実際のアクセスsolrjが行っている
    • Behavior, ConditionBean,  Entity, etc...

SolrFluteで自動生成

  1. u-next/SolrFlute の freegen/solr ディレクトリをdbfluteクライアントのfreegen ディレクトリ内に配置
  2. freegen/ControlFreeGen.vm に以下の記述を追加
  3. Solrのschema.xmlを配置
  4. Solrの設定値を追加
    • xxxx_config.properties, xxxx_env.properties
  5. freeGenMap.dfpropに定義を追加
  6. DBFluteのfreegenを実行
#parse("./solr/ControlFreeGenSolrJava.vm")

SolrFluteでデータ操作

  • Behavior: 操作の種類
  • ConditionBean: クエリを組み立てる部品や検索方法

Behavior

  • selectCount: 検索結果件数の取得
  • selectPage: ページング検索
  • selectFirst: 1件検索
  • selectFacetQuery: ファセット検索
    • 同じfieldに対して複数の条件で検索して、それぞれの結果件数を取得
  • addSolrIndexEntity: Documentをindexに追加
    • 複数追加可能
  • deleteIndex: Indexからdocumentを削除
    • IDを指定して削除するdeleteIndexByIdと全削除のdeleleAllIndex

ConditionBean: General

  • Exists/NotExists: とにかく値が存在する/しない
  • Equal/NotEqual: 指定した値とマッチする/しない
  • InScope/NotInScope: 指定した値の内のどれかに該当する
  • PrefixSearch/SuffixSearch: 先頭/末尾を指定 (foo* / *foo )
  • RangeSearch(from, to): 範囲検索 (始点/終点のみ指定も可)
  • OrScopeQuery: 範囲内の条件をOR検索にする

ConditionBean: dismax

  • 複数fieldに対して同じクエリを適用して個別に検索
  • それぞれの検索結果スコアのうち最も高いものを採用
  • 検索対象fieldに対して重み付けを行うことができる
SolrPagingResultBean<SolrGeneral> list = solrGeneralBhv.selectPage(cb -> {
    cb.query().dismax("君の", queryField -> {
        // queryFieldのvalueはfieldに対する重みを表す
        queryField.put(SolrGeneralDbm.Name, null);
        queryField.put(SolrGeneralDbm.Kana, 10);
        queryField.put(SolrGeneralDbm.NameGeneral, 20);
        queryField.put(SolrGeneralDbm.Synonym, null);
    });
    cb.specify().fieldUid();
    cb.paging(10, 2)
});

ConditionBean: filterQuery (fq)

  • 検索結果に対してさらにAND検索で絞り込みを行う
    • fqの結果は専用のキャッシュに登録されるためクエリ高速化に利用される
  • query()の代わりにfilterQuery()で条件を設定する
SolrPagingResultBean<SolrGeneral> list = solrGeneralBhv.selectPage(cb -> {
    // qパラメータに登録される
    cb.query().dismax("君の", queryField -> {
        queryField.put(SolrGeneralDbm.Name, null);
        queryField.put(SolrGeneralDbm.Kana, 10);
        queryField.put(SolrGeneralDbm.NameGeneral, 20);
        queryField.put(SolrGeneralDbm.Synonym, null);
    });

    cb.specify().fieldUid();

    // fqパラメータに登録される
    cb.filterQuery().setCategory_Equal(someCategory);
    cb.filterQuery().setProductionYear_RangeSearchTo(year);

    cb.paging(10, 2)
});

Faceted Search

  • BehaviorはselectFacetQuery
  • 特定の値でファセットを作る時はaddFacetQuery()で条件を指定する
  • 特定のfieldでファセットを作るときはfacetSpecify()
    SolrFacetResultBean itemFacetByGenre = solrItemBhv.selectFacetQuery(cb -> {
        cb.query().setStatus_Equal("new");
        // addFacetQueryで指定した条件をファセットとして検索
        cb.addFacetQuery(queryBean -> queryBean.setGenre_Equal("pops"));
        cb.addFacetQuery(queryBean -> queryBean.setGenre_Equal("classical"));
        cb.addFacetQuery(queryBean -> queryBean.setGenre_Equal("jazz"));
    });
// => { "pops": 100, "classical": 40, "jazz": 80 } みたいな結果が返ってくる

    SolrFacetResultBean itemFacetByYear = solrItemBhv.selectFacetQuery(cb -> {
        // 存在する発売年ごとのファセットとして検索させる
        cb.facetSpecify().fieldProductionYear();
    });
// => { "2010": 50, "2011": 40, "2012": 20, 
//      "2013": 30, "2014": 10, "2015": 20, "2016": 30 } とかそんな

KVSFlute

  • 生成するもの
  • データアクセス方法の種類
  • 自動生成の手順
  • データ操作と内部の振る舞いについて

KVSFluteが生成するもの

  • KVSに対するデータアクセスの仕組み
    • Behavior, ConditionBean, Entity, etc...
    • 現状はRedisのみ対応
    • Redisへの実際のアクセスはjedisで行っている
  • RDBへのアクセスに対するキャッシュ機構

3種類のデータアクセス方法

  • KVS Store
    • RDBがKVSに置き換わったような感じ
  • KVS Cache
    • RDBの情報を透過的に取得/操作する
  • CB-Embedded Cache
    • DBFluteのColumnNullObject機能を用いてキャッシュを行う
    • DBFluteのConditionBeanやEntityの中にキャッシュ処理が追加される

KVSFluteで自動生成

  • 共通の手順
  • KVS Store
  • KVS Cache
  • CB-Embedded Cache

共通の手順

  1. u-next/KVSFlute の freegen/kvs 及び dbfluteOptional ディレクトリをdbfluteのfreegen ディレクトリ内に配置
  2. freegen/ControlFreeGen.vm に以下の記述を追加
    • a
  3. KVSの接続情報を作成・追加
    • kvs-pool.json, xxxx_config.properties, xxxx_env.properties
  4. freeGenMap.dfpropに定義を追加
  5. DBFluteのfreegenを実行
#parse("./kvs/ControlFreeGenKvsJava.vm")
#parse("./dbfluteOptional/ControlFreeGenDbfluteOptionalJava.vm")

KVS Store

schemaファイルを作成 (schema毎)

// e.g.) kvs-store-schema-sample.json
{
    "sample_schema": {
        "$comment": "schema name",
        "$type": "table",
        "foo": { 
            "type": "String", "comment": "description for this key", "kvsKey": true, "notNull": true
        },
        "bar": { 
            "type": "Integer", "comment": "this is a part of value for associated key", "notNull": true 
        }
    }
}

KVS Store

freeGenMap.dfpropに各schemaに対する定義を追加

; KvsStoreSample = map:{
    ; resourceMap = map:{
        ; baseDir = ../src/main
        ; resourceType = JSON_SCHEMA
        ; resourceFile = $$baseDir$$/resources/kvs/schema/kvs-store-schema-sample.json
    }
    ; outputMap = map:{
        ; templateFile = unused
        ; outputDirectory = $$baseDir$$/java
        ; package = com.example.kvs.store
        ; className = unused
    }
    ; tableMap = map:{
        ; tablePath = map
        ; schema = sample
        ; schemaPrefix = Eg
        ; kvsPoolDiFile = kvs/di/kvs-pool-sample.xml
    }
}

KVS Cache

schemaファイルを作成 (RDBのschema毎)

// e.g.) kvs-cache-schema-sample.json
{
    "target_table": {
        "$comment": "description",
        "singleColumn": {
            "$comment": "description",
            "many": false,
            "kvsKeys": ["kvsKeyNameForSingle"]
        },
        "multipleColumns": {
            "$comment": "description",
            "many": true,
            "kvsKeys": ["kvsKeyNameForMultiple"],
            "orderBy": ["foo", "bar", "baz"]
        }
    }
}

KVS Cache (and CB-Embedded)

freeGenMap.dfpropに各schemaに対する定義を追加

; KvsCacheSample = map:{
    ; resourceMap = map:{
        ; baseDir = ../src/main
        ; resourceType = JSON_SCHEMA
        ; resourceFile = $$baseDir$$/resources/kvs/schema/kvs-cache-schema-sample.json
    }
    ; outputMap = map:{
        ; templateFile = unused
        ; outputDirectory = $$baseDir$$/java
        ; package = com.example.kvs.cache
        ; className = unused
    }
    ; tableMap = map:{
        ; tablePath = map
        ; schema = sample
        ; schemaPrefix =
        ; kvsPoolDiFile = kvs/di/kvs-pool-sample.xml
        ; dbfluteDiFile = dbflute.xml
        ; dbflutePackage = com.example.dbflute
        ; databaseMap = map:{
            ; sample = map:{
                ; schemaDir = ./schema
            }
        }
    }
}

CB-Embedded Cache

littleAdjustmentMap.dfpropにcolumnNullOpjectMapの設定を追加:

対象columnに対して KvsCacheColumnNullObject.getInstance().findColumn() を設定

; columnNullObjectMap = map:{
    ; providerPackage = org.dbflute.kvs.cache
    ; isGearedToSpecify = true
    ; columnMap = map:{
        ; MEMBER_STATUS = map:{
            ; DESCRIPTION = KvsCacheColumnNullObject.getInstance().findColumn(this, "$$columnName$$", $$primaryKey$$)
        }
        ; MEMBER_SECURITY = map:{
            ; REMINDER_ANSWER = KvsCacheColumnNullObject.getInstance().findColumn(this, "$$columnName$$", $$primaryKey$$)
            ; REMINDER_QUESTION = KvsCacheColumnNullObject.getInstance().findColumn(this, "$$columnName$$", $$primaryKey$$)
            ; UPDATE_DATETIME = KvsCacheColumnNullObject.getInstance().findColumn(this, "$$columnName$$", $$primaryKey$$)
        }
    }
}

※これのみfreegenではなく(re)generateを実行

(DBFluteのEntityクラスに組み込まれるため)

KVSFluteでデータ操作

  • KVS Store
    • DBFlute を単純化した感じ
  • KVS Cache
    • キャッシュのkeyとするcolumnを基点に考える感じ
    • 裏側ではRDBとKVSと両方にアプローチしている
  • CB-Embedded Cache
    • いつも通りDBFluteでRDBにアクセスする

KVS Store

  • DBFluteと同じ selectEntity(), selectList()
  • INSERT, UPDATEは insertOrUpdate() (UPSERT)のみ
  • DELETEはEntityを渡してマッチしたものを削除する1件削除のみ
    • queryUpdateとかqueryDelete的なものはない
    • queryUpdate/queryDeleteとは: ConditionBeanを使って検索して、その結果を (まとめて) 更新/削除するためのAPI

KVS Store

// Insert/Update
KvsEgStoreExample insertedEntity = kvsEgStoreExampleBhv.insertOrUpdate(() -> {
    // 対象スキーマのEntity
    KvsEgStoreExample entity = new KvsEgStoreExample();

    entity.setEgId(id);
    entity.setEgName(name);
    entity.setExpireDatetime(expireDatetime);
    return entity;
});
    
String egkey = insertedEntity.getEgkey();

// Select
OptionalEntity<KvsEgStoreExample> result = kvsEgStoreExampleBhv.selectEntity(cb -> {
    // Keyを指定する
    cb.acceptPK(egkey);
});

// Delete
kvsEgStoreExampleBhv.delete(() -> {
    KvsEgStoreExample entity = new KvsEgStoreExample();
    entity.setEgkey(egkey);
    return entity;
});

Inside KVS Store (e.g. SELECT)

// KvsEgBsStoreExampleBhv.java
public OptionalEntity<KvsEgStoreExample> selectEntity(Consumer<KvsEgStoreExampleCB> cbLambda) {
    KvsEgStoreExampleCB cb = new KvsEgStoreExampleCB();
    cbLambda.accept(cb);

    KvsEgStoreExampleDbm kvsEgstoreExampleDbm = asDBMeta();
    kvsEgstoreExampleDbm.validateKeyColumn(cb);

    return examplestoreKvsStoreFacade.findEntity(kvsEgstoreExampleDbm, kvsEgstoreExampleDbm.extractKeyList(cb));
}

// AbstractKvsStoreFacade.java (↑の ExamplestoreKvsStoreFacade の抽象クラス)
public <ENTITY extends KvsStoreEntity> OptionalEntity<ENTITY> findEntity(KvsStoreDBMeta kvsStoreDBMeta, List<Object> serchKeyList) {
     // Manager -> Delegator と続く
    String value = kvsStoreManager.findString(generateKey(kvsStoreDBMeta.getProjectName(), kvsStoreDBMeta.getTableName(), serchKeyList));

    // OptionalEntity (DBFluteのOptional拡張実装) として結果を返却
    if (value == null) {
        return OptionalEntity.empty();
    }
    return OptionalEntity.of(kvsStoreConverterHandler.toEntity(value, kvsStoreDBMeta));
}

// AbstractKvsRedisDelegator.java (jedisを使ってるのはここ)
public String findString(String key) {
    try (Jedis jedis = kvsRedisPool.getResource()) {
        return jedis.get(key);
    }
}

KVS Cache

  • schemaのkvsKeysに設定したcolumnを必ず指定する
    • APIもselectListByXxxx() という風に明示的にこれを指定してくれという風になっている
    • ConditionBeanで指定されていないと例外を吐く
  • kvsKeysによってRDBに対する検索結果がユニークになるか否かで機能が異なる
    • ユニークになるならselectEntityしか使えないとか
    • 複数行ヒットするならデフォルトのORDERを指定できるとか
  • Insert / Update , DeleteはKVS Storeと同じ
    • insertOrUpdateByXxxx(), deleteByXxxx()

KVS Cache

// Insert/Update
kvsProductBhv.insertOrUpdateByCategoryCode(() -> {
    // 対象テーブルのEntity
    Product product = new Product();

    // 必ずkvsKeysに指定したcolumnに値を登録する
    product.setProductCategoryCode(categoryCode);

    product.setProductName(productName);
    product.setProductHandleCode(productHandleCode);
    product.setProductStatusCode_OnSaleProduction();
    product.setRegularPrice(price);

    // DBFluteの機能としてPKは自動裁判してくれる (明示的に指定しても良い)
    return product;
});

// Select (selectList)
List<Product> productList = kvsProductBhv.selectListByCategoryCode(cb -> {
    // 必ず (ry
    cb.query().setProductCategoryCode_Equal(categoryCode);
    cb.query().setProductStatusCode_Equal(statusCode);
    cb.query().addOrderBy_RegisterDatetime_Desc();
});

// Delete
kvsProductBhv.deleteByProductId(() -> {
    Product product = new Product();

    // かならz (ry
    product.setProductCategoryCode(categoryCode);

    return product;
});

KVS Cacheの挙動

  • SELECT
    • まずRedisにクエリを投げる
    • キャッシュあればそれを返し、なければRDBへクエリを投げる
      • RDBへのクエリの結果をRedisに登録した上で返却
  • INSERT / UPDATE, DELETE
    • RDBに対して各操作を実行
    • RDBへの操作実行後、Redisにある操作対象行の(を含む)キャッシュを削除

Inside KVS Cache: SELECT

キャッシュが存在する時

Inside KVS Cache: SELECT

キャッシュが存在しない時

Inside KVS Cache: SELECT

// e.g.) KvsBsProductBhv.java
public List<Product> selectListByCategoryCode(Consumer<KvsProductCB> cbLambda) {
    KvsProductCB kvsCB = createCB(cbLambda);
    ProductCB cb = adjustKvsConditionBeanOfCategoryCode(kvsCB);

    // Redisへのクエリを実行する
    List<Product> list = maihamadbKvsCacheFacade.findList(createKvsKeyListOfCategoryCode(kvsCB), cb);

    Predicate<Product> filter = kvsCB.query().getWherePredicate();
    Comparator<Product> sorted = kvsCB.query().getOrderByComparator();

    // kvsKeysに指定されてないcolumnに対する検索条件もあるのでfilterする
    Stream<Product> stream = list.stream().filter(filter);

    if (sorted != null) {
        stream = stream.sorted(sorted);
    }

    if (kvsCB.xgetFetchFirst().isPresent()) {
        stream = stream.limit(kvsCB.xgetFetchFirst().get());
    }

    return stream.collect(Collectors.toList());
}

// AbstractKvsCacheFacade.java (MaihamadbKvsCacheFacade の抽象クラス)
public <ENTITY extends Entity> List<ENTITY> findList(List<Object> searchKeyList, ConditionBean cb) {
    return kvsCacheBusinessAssist.findList(mySelector(), cb.asDBMeta().getProjectName(), searchKeyList, cb, cacheTtl());
}

Inside KVS Cache: SELECT

// KvsCacheBusinessAssist.java
public <ENTITY extends Entity> List<ENTITY> findList(BehaviorSelector selector, String dbName, List<Object> searchKeyList,
        ConditionBean cb, Integer ttl) {
    final String kvsKey = generateKey(dbName, cb.asTableDbName(), searchKeyList);
    ...

    // この先Manager -> Delegator と続いて AbstractKvsRedisDelegator で jedis.lrange(key, 0, -1) している
    final List<String> found = kvsCacheManager.findList(kvsKey);

    // Redisにキャッシュが存在したらそれをEntityのリストにして返す
    if (isNotEmpty(found)) {
        List<ENTITY> entityList = kvsCacheConverterHandler.toEntityList(found, cb.asDBMeta());
        if (isNotEmpty(entityList)) {
            registerThreadCache(kvsKey, entityList);
            return entityList;
        }
    }

    // キャッシュが存在しなかったらDBFluteでRDBにクエリを投げる
    final List<ENTITY> entityList = (List<ENTITY>) selectList(selector, cb.asDBMeta().getColumnInfoList(), cb);
    // Redisにクエリ結果を登録する
    asyncManager.async(() -> {
        // こちらも Manager -> Delegator と続いて pipeline で del -> rpush -> expireAt と実行している
        kvsCacheManager.registerList(kvsKey, kvsCacheConverterHandler.toMapStringList(entityList), calcAvailableDateTime(ttl));
    });

    return entityList;
}

Inside KVS Cache:

INSERT, UPDATE, DELETE

Inside KVS Cache:

INSERT, UPDATE, DELETE

// e.g.) KvsBsProductBhv.java
public Product insertOrUpdateByCategoryCode(Supplier<Product> entityLambda) {
    Product product = entityLambda.get();
    maihamadbKvsCacheFacade.insertOrUpdate(createKvsKeyListOfCategoryCode(product), product);

    return product;
}

// AbstractKvsCacheFacade.java
public <ENTITY extends Entity> void insertOrUpdate(List<Object> searchKeyList, ENTITY entity) {
    kvsCacheBusinessAssist.insertOrUpdate(mySelector(), entity.asDBMeta().getProjectName(), searchKeyList, entity);
}

// KvsCacheBusinessAssist.java
public <ENTITY extends Entity> void insertOrUpdate(BehaviorSelector selector, String dbName, List<Object> searchKeyList, ENTITY entity) {
    // RDBに対してUPSERTを実行する。delete の場合はここがdelete (queryDelete) になる
    insertOrUpdate(selector, entity);
    removeCache(dbName, entity.asTableDbName(), searchKeyList);
}

protected <ENTITY extends Entity> void removeCache(String dbName, String tableDbName, List<Object> searchKeyList) {
    final String kvsKey = generateKey(dbName, tableDbName, searchKeyList);
    // Insert/Update an entity into(in) KVS asynchronously
    asyncManager.async(() -> {
        // 最終的に jedis.del(key) を呼び出す
        kvsCacheManager.delete(kvsKey);
    });
}

CB-Embedded Cache

  • SELECTだけ
  • DBFlute本体の操作にキャッシュ機能が組み込まれる
  • DBFluteのConditionBeanやEntityをいつも通り使うだけでCB-Embedded Cacheを利用できる
    • 使う側の人が新しく覚えることはなし
  • キャッシュ処理の挙動はKVS Cacheとほぼ同じ

    • Redisにキャッシュが存在するならそれを返却し、なければRDBにクエリを投げて結果をRedisに登録た上で返却

CB-Embedded Cacheの設定

; columnNullObjectMap = map:{
    ; providerPackage = org.dbflute.kvs.cache
    ; isGearedToSpecify = true
    ; columnMap = map:{
        ; MEMBER_STATUS = map:{
            ; DESCRIPTION = KvsCacheColumnNullObject.getInstance().findColumn(this, "$$columnName$$", $$primaryKey$$)
        }
        ; MEMBER_SECURITY = map:{
            ; REMINDER_ANSWER = KvsCacheColumnNullObject.getInstance().findColumn(this, "$$columnName$$", $$primaryKey$$)
            ; REMINDER_QUESTION = KvsCacheColumnNullObject.getInstance().findColumn(this, "$$columnName$$", $$primaryKey$$)
            ; UPDATE_DATETIME = KvsCacheColumnNullObject.getInstance().findColumn(this, "$$columnName$$", $$primaryKey$$)
        }
    }
}

CB-Embedded Cacheの設定

  •  columnNullObjectMapの設定について
    • isGearedToSpecify = true
      • 設定したcolumnのspecify()での取得を空振りさせる
      • SELECTで指定しても取ってこない ((最終的に) nullが返ってくる)
    • 対象columnに対する設定
      • KvsCacheColumnNullObject.getInstance().findColumn()
      • 対象columnにアクセスする時に実行される

Inside CB-Embedded Cache

public <PROP> PROP findColumn(Entity entity, String columnName, Object primaryKey) {
    ...
    // 対象columnのメタ情報を取得する
    final DBMeta dbMeta = entity.asDBMeta();
    final Set<ColumnInfo> specifiedColumnInfoSet = dbMeta.getColumnInfoList()
            .stream()
            .filter(columnInfo -> columnInfo.getPropertyName().equalsIgnoreCase(columnName))
            .limit(1)
            .collect(Collectors.toSet());
    if (specifiedColumnInfoSet.isEmpty()) { specifiedColumnInfoSet.addAll(dbMeta.getColumnInfoList()); }
    ...

    // Redisへクエリの実行; キャッシュが無ければRDBへクエリを投げる
    final OptionalEntity<Entity> optCached = getKvsCacheFacade(dbMeta).findEntityById(primaryKey, dbMeta, specifiedColumnInfoSet);

    if (!optCached.isPresent()) { return null; }

    Entity cached = optCached.get();
    ...

    try {
        PROP value = read(columnName, cached);
        ...
        return value;
    } finally {
        ...
    }
}

Inside CB-Embedded Cache

// KvsCacheBusinessAssist.java
public <ENTITY extends Entity> OptionalEntity<ENTITY> findEntityById(Object id, DBMeta dbmeta, Set<ColumnInfo> specifiedColumnInfoSet) {
    final List<Object> searchKeyList = new ArrayList<Object>(1);
    searchKeyList.add(id);

    // RDBへのクエリ用にDBFluteのBehaviorを取得する
    final BehaviorReadable readable = behaviorSelector.byName(dbmeta.getTableDbName());

    // 複合keyを許容していないのでそれのassert
    final PrimaryInfo primaryInfo = dbmeta.getPrimaryInfo();
    assertOnlyOnePrimaryKey(primaryInfo);

    final ConditionBean cb = readable.newConditionBean();
    final Map<String, Object> primaryKeyMap = new HashMap<String, Object>(1);
    primaryKeyMap.put(primaryInfo.getFirstColumn().getColumnDbName(), id);
    cb.acceptPrimaryKeyMap(primaryKeyMap);

    // クエリを実行して結果を返却
    return kvsCacheBusinessAssist.findEntity(mySelector(), cb.asDBMeta().getProjectName(), searchKeyList, cb, cacheTtl(),
            specifiedColumnInfoSet);
}

最近 (先月くらい)

  • 現場「これはOSSとして公開してもいいのでは!」
  • @jfluteさん「OK 👍」

OSS化に向けての整備

  • 開発用レポジトリから引っぺがす
    • テスト等のためアプリケーションに組み込んだ状態で開発していた
  • 他プロジェクトでも動くかチェック -> 修正 -> ...
  • JavaDocとかコメントの整理、英語化
  • README作成
  • @jfluteさん「example用のレポジトリ作ったからプルリク送っといて!」

Pull Requests to DBFlute-Example

DBFlute fest 2016 (Nov. 27)

https://connpass.com/event/43457/

LTで一足先に紹介しました

DBFlute fest 2016 (Nov. 27)

Released

今後の展望

U-NEXTサービスへの本番適用

  • 現在は2015年のリニューアル当時の実装を利用中
  • 今月から適用しての開発を始める予定

プラグイン開発のやり残し

  • コード見直しと整理
  • JavaDoc英語化の残り
  • 残ったTODOの消化

漠然と考えていること

  • SolrFlute: 機能の充実
    • U-NEXTで必要な機能を優先して実装していた
  • KVSFlute: Redis以外にも対応できるように
    • 需要があればというスタンス

さいごに: We're Hiring!

  • コンテンツ配信プラットフォームの開発に興味がある
  • マイクロサービスアーキテクチャにチャレンジしたい
  • 良いものはどんどん取り入れていきたい
  • DBFlute を使い倒したい

Thanks for Listening!