2.0 への移行#
2.0 では、戻り値を捨てると効かない・黙って何もしないAPI を作り直しました。 壊れ方はコンパイルエラーか例外だけです。黙って意味が変わる変更はしていません。
**2.0 の書き方の多くは 1.5 に先に入っています。**1.5 のうちに書き換えておくと、2.0 に上げたときに直すところが少なくなります。
1. 上げる順番#
- **1.5 に上げて、
./gradlew jimbleCheck --target=2.0を流す。**書き換える行が行番号と新しい書き方つきで出ます。1.5 のうちに直せるもの(J8xx)は直しておきます。 - **2.0 に上げてコンパイルする。**消したもの・型が変わったものは、ここで全部エラーになります(下の 3.)。
- **
./gradlew jimbleCheckをもう一度流す。**2.0 の jimbleCheck は J8xx と J9xx をいつも出します(--target=2.0は付けても付けなくても同じ)。 - **テストを流す。**例外になった書き方(下の 4.)は、コンパイルでは見つかりません。
誤検知は、その行か前の行に // jimble-check:ignore J901 と書くと出なくなります。
補足
2.0 の select は Optional<Data> です。Data row = db.select(...) はコンパイルエラーになるので、見落としません。
迷ったら、1.x と同じ「無ければ null」は db.select(...).orElse(null)、「無ければ 404」は .orElseThrow(() -> new HttpException(404, "...")) です。
2. 1.5 のうちに書き換えられるもの#
置き換え先が 1.5 にあります。2.0 では左の書き方が消えています。
| 1.x | 1.5 からの書き方 | jimbleCheck |
|---|---|---|
new DBTransaction(db) / DBTransaction.transaction(...) |
db.transaction(tx -> { ... }) か try (Tx tx = db.begin()) { ...; tx.commit(); } |
J801 |
transaction.commitEndTransaction() |
tx.commit()(確定して終わる) |
J801 |
transaction.commit()(終わらない) |
tx.checkpoint() |
J801 |
db.beginTransaction() / commitEndTransaction() / rollbackEndTransaction() |
db.begin() / tx.commit() / tx.rollback() |
J802 |
Router admin = router.path("/admin") |
router.path("/admin", admin -> { ... }) |
J803 |
列.subtract(v)(割り算を出す) |
列.divide(v)(引き算なら minus) |
J804 |
Dsl.or(w) / Dsl.and(w) |
Dsl.anyOf(a, b, ...) / Dsl.allOf(a, b, ...) |
J805 |
cookies().put(cookie)(署名しない) |
cookies().putSigned(cookie) / putUnsigned(cookie) |
J806 |
列.eq(null) / not(null) |
列.is_null() / is_not_null() |
J807 |
文字列 "now()" を set(Data) / value(Data) に入れる |
Dsl.now() を値に入れる。平らな行なら setRow(Data) / valueRow(Data) |
J808 |
long id = db.insert(...) |
db.insertKey(...) |
J810 |
context.request().getString("x") |
context.request().bodyAll().getString("x") |
J101 |
Data errors = rules.validate(db, data) |
rules.errors(db, data)(2.0 の validate は「通らなければ 422」) |
—— |
3. コンパイルが通らなくなるもの#
DB#
| 1.x | 2.0 | jimbleCheck |
|---|---|---|
Data row = db.select(...)(0件も失敗も null) |
Optional<Data>。0件は空、失敗は SqlExecuteException。selectCached も同じ |
J901 |
long db.insert(...) |
void。採番値は insertKey(...)。件数が要る INSERT ... SELECT は execute(...) |
J810 |
boolean db.execute(...) |
int(当たった件数。DDL は 0)。失敗は例外 |
J902 |
db.isError() / getError() / isDuplicateKeyError() |
無い。失敗は例外で、一意制約は DuplicateKeyException で受ける |
J903 |
boolean DBUtil.load(...) |
void。繋がらなければ SqlExecuteException(DB_007)で起動が止まる |
J904 |
boolean DBLock.create(...) / lock(...) |
void。失敗は例外 |
J904 |
RedisLockResult RedisLock.tryLock(...) |
Optional<RedisLockResult>(待っても取れなければ空) |
J905 |
DBTransaction・DB の beginTransaction など |
無い(上の 2.) | J801 / J802 |
db.close() が IOException を投げる |
投げない(try の catch (IOException e) がエラーになるので消す) |
—— |
非推奨になったもの(2.0 で @Deprecated(forRemoval = true)。2.x のうちに消します):
| 2.0 で非推奨 | 置き換え | jimbleCheck |
|---|---|---|
selectOrThrow / selectListOrThrow |
select / selectList(同じ意味になった) |
J906 |
insertNoReturnKey |
insert(件数が要るなら execute) |
J906 |
DB.isBatchSuccess(...) |
要らない(失敗は例外) | —— |
RedisLockStatus.Failed |
要らない(lock は例外、tryLock は空) |
—— |
SQL を組む側#
| 1.x | 2.0 | jimbleCheck |
|---|---|---|
列.subtract(v) |
無い。割り算は divide、引き算は minus |
J804 |
Dsl.and(w) / Dsl.or(w) |
無い。Dsl.allOf(...) / Dsl.anyOf(...) |
J805 |
Web#
| 1.x | 2.0 | jimbleCheck |
|---|---|---|
Router.path(String) |
無い。path(パス, admin -> { ... }) |
J803 |
Cookies.put(Cookie) |
無い。putSigned / putUnsigned |
J806 |
Request が Data を継ぐ(request().getString(...)) |
継がない。bodyAll() / body() / bodyQuery() から読む |
J101 |
Data errors = rules.validate(db, data) |
validate は void(通らなければ ValidationException)。一覧は errors(...) |
—— |
検査例外が無くなったもの#
catch (IOException e) や throws CodeException は、投げないものを捕まえているとしてコンパイルエラーになることがあります。消してください。
| 1.x | 2.0 |
|---|---|
CodeException(検査例外) |
RuntimeException の子。catch (CodeException e) はそのまま書ける |
LoadingCache.get() / LoadingCacheMulti.get(k) |
非検査(読み込みの例外は cause) |
Convertor.convert、CsvReader / CsvWriter、XmlBuilder.build、IOUtil.copy / readLines |
非検査(IOException は UncheckedIOException、ほかは CodeException で包む) |
DB.close() / RedisLockResult.close() / ResultSetFetcher.close() |
検査例外を投げない |
ValidationRule / IValidator の throws CodeException |
非検査 |
4. 例外になったもの(コンパイルは通る)#
どれも 1.x では黙って違うことをしていた書き方です。テストで見つけてください。
DB#
| 書き方 | 1.x | 2.0 |
|---|---|---|
| SQL の失敗 | null / -1 / false を返し、isError() で見る |
SqlExecuteException(一意制約は DuplicateKeyException) |
selectList(...) の失敗 |
null |
例外。0件は空リスト(null を返さない) |
update / delete の失敗 |
-1 |
例外。戻り値は件数 |
executeBatch / insertBatch の空の入力 |
null |
空のリスト |
| トランザクションの中で SQL の失敗を catch して続ける | そのままコミットされた | tx.commit() が TransactionException(DB_004)で断り、全部巻き戻す |
DBLock.lock(...) をトランザクションの外で呼ぶ |
鍵が文の終わりで外れ、何も守らなかった | IllegalStateException |
RedisLock.lock(...) で取れない |
status() が Failed |
RedisLockException |
| 枠組みの中の失敗(DB のキャッシュ・キャッシュの無効化・バッチの履歴・マイグレーション) | ログも出ずに捨てていた | 例外 |
補足
**Tx は例外で抜けたら巻き戻ります。**途中で HttpException を投げるのに、自分で rollback を書く必要はありません。
一意制約を「あれば更新」に使うなら、トランザクションの外で catch (DuplicateKeyException e) するか、INSERT ... ON CONFLICT を使ってください(中で受け止めると、その Tx は確定できません)。
SQL を組む側#
| 書き方 | 1.x | 2.0 |
|---|---|---|
列.eq(null) / not(null) |
= NULL を組み、どの行にも当たらない |
SqlBuildException。is_null() / is_not_null() |
where(Data) の空の値(null・空の配列) |
= NULL などを組んでいた |
例外(is_null / is_not_null / between / in / not_in は別) |
where(Data) に "where" キーの無い Data |
条件が付かずに全件 | 例外(空の Data は何もしない)。Select / Update / Delete すべて |
set(Data) / value(Data) に包まない行・別のテーブルの分だけの Data |
黙って何も入れない | 例外。平らな行は setRow / valueRow |
文字列 "now()" |
現在時刻になった | ただの文字列。現在時刻は Dsl.now() |
apply(Data) は句をまとめて読むので、無い句は無いままです(例外にしません)。
Data#
| 書き方 | 1.x | 2.0 |
|---|---|---|
getInt などで読めない値("abc"、int に "1.5"、桁あふれ、真偽に "yes") |
黙って 0 / false / null |
DataConversionException |
getEnum で一致しない |
null |
例外。無くてよいなら getEnumOptional(key, 型) |
Data.fromJsonString / Dson.decodes に壊れた JSON |
null か途中まで |
JsonParseException(空文字と "null" は null) |
型を変えて読む(getStringList で数の一覧を読む、JSON の文字を getData で読む) |
変換した値を書き戻していた(読んだだけで JSON の出力が変わった) | 書き戻さない |
[!IMPORTANT] **無いキー(キーが無い・
null・空文字)は、これまでどおり0/false/nullです。**例外になるのは「あるのに読めない」ときだけです。 利用者の入力をgetIntなどで読むなら、先にバリデーションを通してください。通さずに読んで例外になると 500 です。?page=abcのpaging()は枠組みの側で寛容に読み、1 ページ目にします。
getDataOptional / getStringListOptional などの Optional 版は、名前どおり「無ければ空を作って入れる」を残しています(data.getDataOptional("x").put(...) と書いた値は残ります)。あった値は書き換えません。
Web#
| 書き方 | 1.x | 2.0 |
|---|---|---|
rules.validate(db, data); で通らない |
一覧を返すだけ(捨てると素通り) | ValidationException。枠組みが 422 で {"validation": {項目: [メッセージ]}} を返す |
required() / empty() でキーごと送られない |
素通り | 失敗(insertRequired() の規則は「登録のときだけ必須」のまま) |
返し方を2つ積む(json(...) と redirect(...) など) |
最初の1つだけ返り、残りは捨てた | 2つ目を積んだ時点で IllegalStateException(同じ種類を重ねるのはよい) |
| 送ったあとのヘッダ・Cookie | 届いていなかった | 例外 |
application/json の本文が読めない |
空の Data | body() / bodyJson() / bodyAll() が 400 の HttpException(MCP は PARSE_ERROR) |
session().destroy() のあとの put / remove / clear |
保存されなかった | IllegalStateException |
session().data().put(...) |
「変えた」印が付かず保存されなかった | UnsupportedOperationException(読み取り専用の写し)。session().put(...) を使う |
paging() のあとの paging(50) |
50 が効かなかった | IllegalStateException |
AbstractExecutor.cancel() |
印を立てるだけで、あとの行も走った | そこで抜けて onCancel へ |
422 の本文を自分で作りたいときは、errors(...) で一覧を受け取ってください(1.5 の errors と同じです)。
一覧の検査は errors(db, List) / validate(db, List) です(本文は {"rows": [...]})。
5. jimbleCheck の変わったところ#
- J8xx と J9xx をいつも出します(
--target=2.0は受け付けて何もしません)。 - J901 / J905 は、
Optionalとして受け取っている書き方(.orElse(...)、Optional<...> x =など)は出しません。 - J906 から
selectCached/selectListCachedを外しました(名前はそのまま。戻り値の形がselect/selectListと揃っただけです)。 - J907 を足しました:
session().data().put(...)など(2.0 で例外)。 - J809(
validateの戻り値を捨てている)を外しました。文として書くvalidate(...)が 2.0 の正しい書き方です。
6. 1.5 で出るようになった警告#
1.5 は、2.0 で例外になる書き方をプロセスで1度だけログに出していました(呼び出し元の行つき)。 1.5 で警告が出ていなければ、上の 4. の多くには当たっていません。
| 1.5 の警告 | 2.0 |
|---|---|
eq(null) は「= NULL」を組み… |
SqlBuildException |
set(Data) は {"set": …} の形を読みます |
例外 |
set(Data) は文字列 "now()" を… |
ただの文字列 |
返し方が2つ以上積まれています |
IllegalStateException |
送ったあとにヘッダ… / 応答を送ったあとに Cookie… |
例外 |
本文の JSON を読めませんでした |
400 |
paging(50) の件数は効きません |
IllegalStateException |
request().getString("x") は送られてきた値を読みません |
コンパイルエラー |
required() / empty() はキーが無いと検査しません |
失敗 |
destroy() のあとにセッションを変えても保存されません |
IllegalStateException |