DBFlute on Scala

TODO jflute まだまだ作り途中です

ポリシー、十か条

DB変更に強いO/Rマッパー、というテーマは崩さずに...

1.0.x系、1.1系
1.0.xはパイロット版、Java8 (1.1) になってからが本気
メインターゲット
JavaやC#でDBFluteに親しんでいた人
サブターゲット
Scalaやりながらもデータモデルを大切にする人
Javaとの互換
もちろん互換は崩すが、あまりにかけ離れたものにはしない。※1.1仕様が前提
Scalaっぽさ
できる限り追従するが、DBFluteらしさを損ねてまではやらない
Immutable
アプリに直接戻すものはImmutable, Callback限定スコープでMutable
DBFluteの環境
自動生成エンジンとランタイムはJavaと共有、自動生成クラスがScala
ConditionBean
Callbackなどは改善して、他はわりとJavaのまま ★要検討
Entity
検索用(Immutable)、更新用(Mutable)。NullableカラムはOption型
Behavior
GoogleGuice管理。シングルトンからも取得できるように
プログラム型
ScalaのIntやLongを利用、日付はJodaTime

というポリシーのもと、リーン・スタートアップ、インクリメンタル開発で進めていきます。

とにかく検索は?

ConditionBean で一件検索 *selectEntity(cb)

e.g. 会員ID "1" 番で会員を一件検索 (関連テーブルの会員ステータスも一緒に取得) @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// 一緒に取得する関連テーブル : 会員ステータス
// 絞り込み条件 : 会員IDが "1" 番である
// _/_/_/_/_/_/_/_/_/_/
val member = DBFlutist.memberBhv.selectEntity { cb =>
  cb.setupSelect_MemberStatus
  cb.query.setMemberId_Equal(1)
}
member.foreach { mb =>          // 会員: Option[Member] (Immutable)
  ... = mb.memberId             // *会員ID       : Int
  ... = mb.memberName           // *会員名称      : String
  ... = mb.memberAccount        // *会員アカウント : String
  ... = mb.birthdate            // 生年月日       : Option[LocalDate]
  ... = mb.formalizedDatetime   // 正式会員日時    : Option[LocalDateTime]
  if (mb.isMemberStatusCode_Formalized) { // 会員ステータスの区分値判定
    ...
  }
  ... = mb.memberStatus.map(_.memberStatusName) // 会員ステータス: Option
  ...
}

ConditionBean でリスト検索 *selectList(cb)

e.g. 色々な絞り込み条件で会員をリスト検索 (関連テーブルもいろいろと) @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// 一緒に取得する関連テーブル:
//  o 会員ステータス
//  o 会員サービスと、その親テーブルのサービスランク
// 絞り込み条件:
//  o 会員名称が "S" で始まる
//  o 会員ステータスが "正式会員" である (区分値メソッド)
//  o 支払い済みで200円以上の購入をしたことがある会員 (one-to-many)
// ソート条件:
//  o 生年月日の降順 (でも null は後ろにね)
//  o 会員IDの昇順
// _/_/_/_/_/_/_/_/_/_/
val memberList = DBFlutist.memberBhv.selectList { cb =>
  cb.setupSelect_MemberStatus
  cb.setupSelect_MemberServiceAsOne.withServiceRank

  cb.query.setMemberName_PrefixSearch("S")
  cb.query.setMemberStatusCode_Equal_Formalized

  cb.query.existsPurchaseList { purchaseCB =>
    purchaseCB.query.setPurchasePrice_GreaterEqual(200)
    purchaseCB.query.setPaymentCompleteFlg_Equal_True
  }

  cb.query.addOrderBy_Birthdate_Desc.withNullsLast
  cb.query.addOrderBy_MemberId_Asc
}
memberList.foreach { mb =>
  ... = mb.memberName
  ... = mb.memberStatus.map(_.memberStatusName)
  ... = mb.memberServiceAsOne.map(_.serviceRank.map(_.serviceRankName))
}

ConditionBean でページング検索 *selectPage(cb)

e.g. 会員名称に "vi" が含まれる会員をページング検索 @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// 一緒に取得する関連テーブル
//  o 最終ログイン日時
//  o 支払済みである購入の平均購入価格
// 絞り込み条件
//  o 購入に対する支払で、一回で3000円以上もの金額を支払ったことがある会員
// ソート条件 : 最終ログイン日時の降順
// _/_/_/_/_/_/_/_/_/_/
val page = DBFlutist.memberBhv.selectPage { cb =>
  cb.specify.derivedMemberLoginList.max { subCB =>
    subCB.specify.columnLoginDatetime
  }(Member.ALIAS_lastestLoginDatetime)

  cb.specify.derivedPurchaseList.avg { purchaseCB =>
    purchaseCB.specify.columnPurchasePrice
    purchaseCB.query.setPaymentCompleteFlg_Equal_True
  }(Member.ALIAS_purchasePriceAverage)

  cb.query.existsPurchaseList { purchaseCB =>
    purchaseCB.query.existsPurchasePaymentList { paymentCB =>
      paymentCB.query.setPaymentAmount_GreaterEqual(3000)
    }
  }

  cb.query.addSpecifiedDerivedOrderBy_Desc(Member.ALIAS_lastestLoginDatetime)
}
page.foreach { mb =>
  ... = mb.memberName
  ... = mb.lastestLoginDatetime
  ... = mb.averagePurchasePrice
}
val allRecordCount = page.allRecordCount
val allPageCount = page.allPageCount
if (page.hasPreviousPage) {
  ...
}

ConditionBean でカーソル検索 *selectCursor(cb)(handler)

e.g. 会員名称に "vi" が含まれる会員をカーソル検索 @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// 一緒に取得する関連テーブル
//  o 現在の会員住所情報(業務的one-to-one)
// 絞り込み条件:
//  o 会員名称に "vi" が含まれる
//  o 二十歳になってから正式会員になった会員
// ソート条件:
//  o 会員ステータスの表示順
//  o 会員サービスのサービスポイント数の降順
// _/_/_/_/_/_/_/_/_/_/
DBFlutist.memberBhv.selectCursor { cb =>
  cb.setupSelect_MemberAddressAsValid(currentDate)

  cb.query.setMemberName_LikeSearch("vi")(_.likeContain)
  cb.columnQuery(_.specify.columnFormalizedDatetime)
    .greaterThan(_.specify.columnBirthdate).convert(_.addYear(20))

  cb.query.queryMemberStatus.addOrderBy_DisplayOrder_Asc
  cb.query.queryMemberServiceAsOne.addOrderBy_ServicePointCount_Desc
} { mb =>
  ... = mb.memberName
  ... = mb.memberAddressAsOne.map(_.address)
}

子テーブルの検索 (LoadReferrer) *selectXxx(cb)(loader)

e.g. 会員をリスト検索して、子テーブルもいろいろと @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// 一緒に取得する関連テーブル:
//  o 会員ステータス (その子テーブルである会員ログインも取得される予定)
//  o 購入と商品 (購入日時の降順で) (one-to-many, one-to-many-to-one)
//  o 購入支払 (支払日時の降順で) (one-to-many-to-many)
//  o 会員ステータス経由の会員ログイン (ログイン日時の降順で) (many-to-one-to-many)
// 絞り込み条件:
//  o 会員名称が "S" で始まる
// ソート条件:
//  o 会員IDの昇順
// _/_/_/_/_/_/_/_/_/_/
val memberList = DBFlutist.memberBhv.selectList { cb =>
  cb.setupSelect_MemberStatus // 後で pullout されるので必要
  cb.query.setMemberName_PrefixSearch("S")
  cb.query.addOrderBy_MemberId_Asc
} { loader =>
  loader.loadPurchaseList { purchaseCB =>
    purchaseCB.setupSelect_Product
    purchaseCB.query.addOrderBy_PurchaseDatetime_desc
  }.withNestedReferrer { loader =>
    loader.loadPurcasePaymentList { paymentCB =>
      paymentCB.query.addOrderBy_PaymentDatetime_desc
    }
  }
  loader.pulloutMemberStatus.loadMemberLoginList { loginCB =>
    loginCB.query.addOrderBy_LoginDatetime_Desc
  }
}
memberList.foreach { mb =>
  ... = mb.memberName
  ... = mb.purchaseList.foreach { pc =>
    ... = pc.product
    ... = pc.purchasePaymentList.foreach { pay =>
      ... = pay.paymentAmount
      ... = pay.paymentDatetime
    }
  }
}

そういえば更新とかは?

コンパニオンオブジェクトによる apply() での指定も検討しましたが、ダミー値運用を許容しないと登録カラムや更新カラムの限定化ができないため、 Callback限定スコープMutableで実現しました。また、その方が、区分値メソッドの恩恵も受けられるので。

一件登録: insert()

e.g. 会員を一件登録 @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// Xavi (シャビ) という名の仮会員を登録
//  o PKは自動採番
//  o 共通カラムはdfpropの通りに自動設定
//  o バージョン番号は初期値で自動設定
// _/_/_/_/_/_/_/_/_/_/
DBFlutist.memberBhv.insert { mb =>
  mb.memberName = "Xavi"
  mb.memberAccount = "Passer"
  mb.birthdate = Option(...)
  mb.formalizedDatetime = None
  mb.memberStatusCode_Provisional
}
// ※設定されたカラムだけinsert文に列挙される
// (未設定カラムは null もしくはデフォルト値で登録される)

排他制御あり一件更新: update()

e.g. 会員を排他制御ありで一件更新 @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// 会員ID "7" 番の会員を、会員名称を Xavi (シャビ) にして正式会員に更新
//  o 共通カラムはdfpropの通りに自動設定
//  o バージョン番号で排他制御 (楽観的並行性制御)
//  o バージョン番号は自動インクリメント
// _/_/_/_/_/_/_/_/_/_/
DBFlutist.memberBhv.update { mb =>
  mb.memberId = 7
  mb.memberName = "Xavi"
  mb.memberStatusCode_Formalized
  mb.versionNo = ... // 編集画面表示時に検索されたバージョン番号
}
// ※設定されたカラムだけupdate文のset句に列挙される (未設定カラムは何も更新されない)

排他制御なし一件更新: updateNonstrict()

e.g. 会員を排他制御なしで一件更新 @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// 会員ID "7" 番の会員を、会員名称を Xavi (シャビ) にして正式会員に更新
//  o 共通カラムはdfpropの通りに自動設定
//  o バージョン番号は自動インクリメント
// _/_/_/_/_/_/_/_/_/_/
DBFlutist.memberBhv.updateNonstrict { mb =>
  mb.memberId = 7
  mb.memberName = "Xavi"
  mb.memberStatusCode_Formalized
}
// ※設定されたカラムだけupdate文のset句に列挙される (未設定カラムは何も更新されない)

排他制御あり一件削除: delete()

e.g. 会員を排他制御ありで一件削除 @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// 会員ID "7" 番の会員を削除
//  o バージョン番号で排他制御 (楽観的並行性制御)
// _/_/_/_/_/_/_/_/_/_/
DBFlutist.memberBhv.delete { mb =>
  mb.memberId = 7
  mb.versionNo = ... // 編集画面表示時に検索されたバージョン番号
}

※deleteNonstrict() は、排他制御はされないこと以外は同じ

※deleteNonstrictIgnoreDeleted() は、1.1以降はサポートされない

★要検討:PKとバージョン番号だけなので、Tupleとか使って指定する方がいいかな?(どうせ検索Entityは突っ込めないので...)

バッチ登録: batchInsert()

e.g. 会員をバッチ登録 @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// いろいろな会員を一括で登録
//  o 共通カラムはdfpropの通りに自動設定
//  o バージョン番号で排他制御 (楽観的並行性制御)
//  o バージョン番号は自動インクリメント
// _/_/_/_/_/_/_/_/_/_/
DBFlutist.memberBhv.batchUpdate { batch =>
  batch.add { mb =>
    mb.memberName = "Xavi"
    mb.memberStatusCode_Formalized
    ...
  }
  batch.add { mb =>
    mb.memberName = "Iniesta"
    mb.memberStatusCode_Provisinal
    ...
  }
  batch.add { mb =>
    mb.memberName = "Akagi"
    mb.memberStatusCode_Withdrawal
    ...
  }
}
// ※設定されたカラムの最小公倍数のカラムセットがinsert文のset句に列挙される
// (すべてのレコードで未設定カラムは null もしくはデフォルト値で登録される)

排他制御ありバッチ更新: batchUpdate()

e.g. 会員を排他制御ありでバッチ更新 @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// いろいろな会員を一括で更新
//  o 共通カラムはdfpropの通りに自動設定
//  o バージョン番号で排他制御 (楽観的並行性制御)
//  o バージョン番号は自動インクリメント
// _/_/_/_/_/_/_/_/_/_/
DBFlutist.memberBhv.batchUpdate { batch =>
  batch.add { mb =>
    mb.memberId = 1
    mb.memberStatusCode_Formalized
    mb.versionNo = ... // 編集画面表示時に検索されたバージョン番号
  }
  batch.add { mb =>
    mb.memberId = 3
    mb.memberStatusCode_Provisinal
    mb.versionNo = ... // 編集画面表示時に検索されたバージョン番号
  }
  batch.add { mb =>
    mb.memberId = 7
    mb.memberStatusCode_Withdrawal
    mb.versionNo = ... // 編集画面表示時に検索されたバージョン番号
  }
}
// ※設定されたカラムだけupdate文のset句に列挙される (未設定カラムは何も更新されない)
// ※全てのレコードにおいて、設定されたカラムセットが同じである必要がある

※batchUpdateNonstrict() は、排他制御はされないこと以外は同じ

ConditionBeanで更新: queryUpdate(entity, cb)

e.g. 会員をConditionBeanの条件で一気に更新 @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// すべての仮会員を、正式会員にして、名前もシャビに更新してしまう
//  o 共通カラムはdfpropの通りに自動設定
//  o 排他制御はされない、バージョン番号は自動インクリメント
//  o 全レコード更新は許されない
// _/_/_/_/_/_/_/_/_/_/
DBFlutist.memberBhv.queryUpdate { mb =>
  mb.memberName = "Xavi"
  mb.memberStatusCode_Formalized
} { cb =>
  cb.query.setMemberStatusCode_Provisional
}
// ※設定されたカラムだけupdate文のset句に列挙される (未設定カラムは何も更新されない)

ConditionBeanで削除: queryDelete(cb)

e.g. 会員をConditionBeanの条件で一気に削除 @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// すべての仮会員を、削除する
//  o 排他制御はされない
//  o 全レコード削除は許されない
// _/_/_/_/_/_/_/_/_/_/
DBFlutist.memberBhv.queryDelete { cb =>
  cb.query.setMemberStatusCode_Provisional
}

オプション付き一件更新: update(entity)(option)

e.g. 購入をoption付きで一件更新 @Scala
...
// _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
// 購入ID "7" 番の購入の購入数をインクリメント
//  o option でインクリメントするカラムを指定
// _/_/_/_/_/_/_/_/_/_/
DBFlutist.purchaseBhv.update { pur =>
  pur.purchaseId = 23
  pur.versionNo = ...
} { option =>
  option.self(_.specify.columnPurchaseCount).plus(1)
}
// ※購入数(PurchaseCount)は、"PURCHASE_COUNT = PURCHASE_COUNT + 1" される