Golang Gorm 実践メモ&注意点
Golang Gorm 実践メモ&注意点
随時追記
公式ドキュメントhttps://gorm.io/ja_JP/docs/index.html
- Golang のデファクトスタンダード的ポジションのORM
- テーブルとオブジェクトの紐づけ。SQL実行。両方提供 = 単体でCORS実装可能
注意事項
Create、Update、Save の振舞いの違い
Create: 新規作成
- Update: 空値が除外、任意のカラムのみ選択可能
- Save: 空値も含めて全てのカラムを一律保存
使い分けの方針
- 新規レコード作成時: Create
- 既存レコード更新時: Update
- 更新時に空値を含めてStructで更新: Save
- 更新時に空値を含めてMapで更新: Update
Saveは、なければ新規作成、あれば更新。となるため注意が必要
- (コールバックで)自動的に関連レコードが全て保存される
関連レコード含めて全て更新される
回避: 関連レコードの一括保存や、そのほかBeforeCreateのようなコールバックメソッドを呼ばずに更新したい場合、 UpdateColumn、UpdateColumnsを使用
- マイグレーション機能は本番向きでない
一回きりのマイグレーションで完結する場合は十分かもしれないが、長期的な運用の管理には機能不足
migrate 等、別のツールを使う
- Query Conditions関数は、必ずWhere() を使用して条件を指定
db.Where("id = ?", 1).Find(&users)
様々な書き方ができるように設計されているが
db.Where("id = ?", "1").Find(&users) // SELECT * FROM `users` WHERE (id = '1') db.Where(User{ID: 1}).Find(&users) // SELECT * FROM `users` WHERE (`users`.`id` = 1) db.Find(&users, "id = ?", "1") // SELECT * FROM `users` WHERE (id = '1') db.Find(&users, User{ID: 1}) // SELECT * FROM `users` WHERE (`users`.`id` = 1) db.Find(&users, "1") // SELECT * FROM `users` WHERE (`users`.`id` = '1')
5番目の書き方トラップがあり、以下のように書くと任意のSQL実行できてしまう。
userInputID := "1=1" db.Find(&users, userInputID) // SELECT * FROM `users` WHERE 1=1 ※全権取得になる userInputID := "1=1);DROP table users;--" db.Find(&users, userInputID) // SELECT * FROM `users` WHERE (1=1);DROP table users;--) ※usersテーブル削除
ref
DDD 実践メモ
DDD 実践メモ
随時追記
DDDのパッケージ構成
以下が参考になる
Repositoryのinterfaceはdomain層とQueryServiceのinterfaceはapplication層に置く。それによりrepository層の直依存を切れる。repository→application/domain層への依存を最小化できる。関心の分離
リポジトリ
- NGパターン
- 1エンティティに対して複数Repository作る -> データ/ロジックの整合性が取れない
- 対策:状態更新に関わる振る舞いはEntityやServiceに実装
- 子テーブルにもRepositoryを作成する -> ビジネス要件のチェックロジックがrepositoryにも持たざるを得なくなったり、チェック処理を迂回して更新ができてしまう
- 対策:集約を使う
- 複雑なクエリをRepositoryで発行する -> メンテナンスしづらい、かつ N+1問題にもつながりかねない
- 対策:CQRS
- 1エンティティに対して複数Repository作る -> データ/ロジックの整合性が取れない
Repository は集約の単位で作成
ref:
集約
- 永続化の単位となるクラス群。境界の定義
- 1トランザクション範囲。(トランザクション整合性)
- 集約の操作は集約ルートを介してのみ可能
- 集約は小さくし、トランザクションの範囲は最小限にする。->トランザクションの衝突が少なく、スケーラビリティ/性能が良く保てる
- 別の集約ルートはIDのみ保持して参照する->値の同時更新が必要ならEntityを持ち、参照のみで済むならID保持(参照するデータが変わるならID変えるだけで済む)
以下に立ち向かう方法が集約:
- サービスクラスに大量の Repository をインジェクションが必要に…
- オブジェクトの一部だけをロードして、子要素が NULL の状態に…
- データアクセスの設定/記述方法により N + 1 問題発生…
例:
orderRepository.save(order); order.orderDetails.forEach(orderDetailRepository::save);
を、1リポジトリで両方の永続化を行うことで以下となり、コードの複数箇所で Order と OrderDetail 保存する場合にも、1Repository の内部実装にて共通化可能
orderRepository.save(order);
ref:
-
CQRS
- 部分的導入が可能
- QuerySerivceの戻り値がユースケースに依存するものなためUseCase層
以下が参考になる
reference indexes
-
- 具体的サンプルがある
indexnext |previous |TERASOLUNA Server Framework for Java (5.x) - ドメイン層の実装
-
- サンプル問題と例有り
実践! Typescript で DDD - マイクロサービス設計のすすめ
- マイクロサービスを考えるなら一読した方が良いかも
-
- ドメインロジック考える時の参考になりそう
最近の海外DDDセミナーを聞いてみたら色々と常識が破壊された
- イベントが設計の基本線となりつつあるらしい… pub/subの設計
Golang コード自動生成
Golang コード自動生成
モチベーション:単調なコード追加の繰り返しはコード自動生成に置き換えたい
Golang のコードから Golang のコードを生成する
単なるCRUDのコード実装だけなら(ビジネスロジックは関係ないため)
- domain層にentity追加
以下は(単純な追加だけなので)自動生成に置き換えられるはず
- domain層に追加したentityに対するRepositoryインターフェース
- entityのfactory
- Repositoryインターフェースの実装
- 実装したコードのテスト
AST(抽象構文木)を利用するのが正確な解析&生成ができて良さそう。
コードからの情報取得
ソースコードをparse -> AST(抽象構文木)にした後で解析して取得
- (構文木として意味を持った状態で扱えるため) 欲しいノードが探しやすい。正確に取得可
func main() { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "./example/example.go", nil, 0) // 第4引数はMode if err != nil { fmt.Println(err) return } // 最初に見つかったstruct名を入れる var name string ast.Inspect(f, func(n ast.Node) bool { x, ok := n.(*ast.TypeSpec) // type xxxx の型宣言部分取得 if !ok { return true } if _, ok := x.Type.(*ast.StructType); ok { // 取れた type が struct なら その名前取得 if name == "" { name = x.Name.String() } } return true }) fmt.Println("struct name:", name) }
構文解析参考:
コード生成
AST(抽象構文木)を組み立てる
- 構文木として意味を持った状態で扱えるため、文法ミスがない
- ※テンプレートエンジン使用での実現方法は、テンプレートの構文間違えていても気付きにくい
ASTだと、以下の少量だけでも多量のコードを書く必要がある。 jennifer (code generator) を使用すればシンプルに書ける。
package main import "fmt" func main() { fmt.Println("Hello, 世界") }
のコードを生成するのに、
- ASTの場合:
package main import ( "go/ast" "go/format" "go/token" "os" "strconv" ) func main() { f := &ast.File{ Name: ast.NewIdent("main"), Decls: []ast.Decl{ &ast.GenDecl{ Tok: token.IMPORT, // import 部 Specs: []ast.Spec{ &ast.ImportSpec{ Path: &ast.BasicLit{ Kind: token.STRING, Value: strconv.Quote("fmt"), }, }, }, }, &ast.FuncDecl{ Name: ast.NewIdent("main"), // main 関数部 Type: &ast.FuncType{}, Body: &ast.BlockStmt{ List: []ast.Stmt{ &ast.ExprStmt{ X: &ast.CallExpr{ Fun: &ast.SelectorExpr{ X: ast.NewIdent("fmt"), Sel: ast.NewIdent("Println"), }, Args: []ast.Expr{ &ast.BasicLit{ Kind: token.STRING, Value: strconv.Quote("Hello, 世界"), }, }, }, }, }, }, }, }, } format.Node(os.Stdout, token.NewFileSet(), f) }
ref: Goの抽象構文木(AST)を手入力してHello, Worldを作る #golang
- jennifer の場合:
package main import ( "fmt" "github.com/dave/jennifer/jen" ) func main() { f := NewFile("main") f.Func().Id("main").Params().Block( Qual("fmt", "Println").Call(Lit("Hello, world")), ) fmt.Printf("%#v", f) }
ref
Elasticsearch メモ
Elasticsearch メモ
概要
- Elastic Stack の 1製品
- JSONベースの高速検索を可能とする検索・分析エンジン
用途: ブログ/記事投稿サイト等のWebサービスの全文検索+α?に使われている模様
メモ: - RDBに被せて使える物でもなく、利用するなら併用する形式 - RDBとは別サーバリソースが必要 - RDBのデータをElasticsearchに同期的に投入する必要がある
RDBの代替にはならない
Elasticsearchのデータストアとしての振舞いの特徴
- ドキュメント型データベース
- スケーラブル
- トランザクションがない
- 結合は不得手
大量の文書を高速に検索することに適した仕組みを活かして、限定的に使用するのが良い
Elasticsearchは速度改善だけで選ぶものではない
- Elasticsearchにマッチする要件
上記はRDBでは対応が難しい場合が多く、文章の検索に特化したElasticsearchならではの活用範囲
ref
MySQL(Replication Protocol)とElasticsearchのほぼリアルタイム連携の実現(リアルタイム・インデクシング)
大量データを検索するサービスでElasticsearchはRDBの代替候補になりうるか?(Elasticsearch vs pg_bigm) ※連載
Java Assertj + Mockito
Java Assertj + Mockito
Assertj
共通
- 同値: isEqualTo
assertThat("Foo").isEqualTo("Foo"); assertThat("Foo").isNotEqualTo("Bar");
- Null: isNull/isNotNull
assertThat(actual).isNull(); assertThat(actual).isNotNull();
- 同一インスタンス: isSameAs/isNotSameAs
assertThat(bar1).isSameAs(bar2); assertThat(bar1).isNotSameAs(bar2);
- インスタンスの型: isInstanceOf/isNotInstanceOf
assertThat(baz).isInstanceOf(Baz.class); assertThat(qux).isInstanceOf(Baz.class).isInstanceOf(Qux.class); assertThat(qux).isNotInstanceOf(Baz.class);
- toString値: hasToString
assertThat(fooBar).hasToString("FooBar"); // toString()の値確認
- 注釈: as
assertThat("Foo").as("AssertJ sample").isEqualTo("Bar");
文字列
- 先頭/末尾一致
assertThat("FooBar").startsWith("Foo"); assertThat("FooBar").endsWith("Bar");
- 大小無視の一致
assertThat("Foo").isEqualToIgnoringCase("FOO");
- 空文字/Null
assertThat("").isEmpty(); assertThat(actual).isNullOrEmpty(); // null
- 正規表現比較
assertThat("FooBarBaz").matches("F..B..B..").matches("F.*z");
- 数字かどうか
assertThat("1234567890").containsOnlyDigits();
- 行数確認
assertThat("foo\nbar\nbaz").hasLineCount(3); assertThat("foo\r\nbar\r\nbaz").hasLineCount(3);
数値
- 範囲(Between)
assertThat(7).isBetween(0, 9).isBetween(7, 7); assertThat(7).isCloseTo(5, within(2)); // 5 ± 2
- 大なり/小なり
assertThat(7).isGreaterThan(6).isGreaterThanOrEqualTo(7); assertThat(7).isLessThan(8).isLessThanOrEqualTo(7);
Collection(List/Set等)
- hasSize: サイズ確認
assertThat(actuals).hasSize(4);
- isEmpty: 空か
assertThat(actuals).isEmpty();
- contains: 並び順は検証しない。含まれていればOK
List<String> actuals = Lists.newArrayList("Lucy", "Debit", "Anna", "Jack"); assertThat(actuals).contains("Lucy", "Anna");
- containsOnly: 並び順は検証しない。全て含むならOK
List<String> actuals = Lists.newArrayList("Lucy", "Debit", "Anna", "Jack"); assertThat(actuals).containsOnly("Debit", " Lucy", "Jack", "Anna");
- containsSequence: 並び順を検証。件数確認しない
List<String> actuals = Lists.newArrayList("Lucy", "Debit", "Anna", "Jack"); assertThat(actuals).containsSequence("Lucy", "Debit");
- containsSubSequence: 並び順を検証。件数確認しない。抜け漏れOK
List<String> actuals = Lists.newArrayList("Lucy", "Debit", "Anna", "Jack"); assertThat(actuals).containsSubsequence("Lucy", "Anna") .containsSubsequence("Lucy", "Jack");
- containsOnlyOnce: 含む かつ 重複なしならOK
List<String> actuals = Lists.newArrayList("Lucy", "Debit", "Anna", "Lucy"); assertThat(actuals).containsOnlyOnce("Debit", "Anna");
- containsAnyOf: いずれか1つがある
assertThat(actuals).containsAny("Debit", "Anna");
- extracting
// 特定フィールドのみ抽出 assertThat(list).extracting("name").containsExactly("佐藤","田中","鈴木");
Map
- containsEntry
assertThat(actuals).containsEntry("Key1", 101) .containsEntry("Key2", 202) .doesNotContainEntry("Key9", 999);
- containsKey
assertThat(actuals).containsKeys("Key2", "Key3") .doesNotContainKey("Key9");
- containsValue
assertThat(actuals).containsValues(202, 303) .doesNotContainValue(999);
- hasSize/isEmpty もある
Iterable関係
まとめて検証
- allSatisfy: 全て満たす
assertThat(hobbits).allSatisfy(character -> {
assertThat(character.getRace()).isEqualTo(HOBBIT);
assertThat(character.getName()).isNotEqualTo("Sauron");
});
- anySatisfy: いずれかを満たす
assertThat(hobbits).anySatisfy(character -> {
assertThat(character.getRace()).isEqualTo(HOBBIT);
assertThat(character.getName()).isEqualTo("Sam");
});
- noneSatisfy: 全て満たさない
assertThat(hobbits).noneSatisfy(character -> assertThat(character.getRace()).isEqualTo(ELF));
特定の1つを検証
- first/element/last: 最初/間/最後
Iterable<TolkienCharacter> hobbits = list(frodo, sam, pippin);
assertThat(hobbits).first().isEqualTo(frodo);
assertThat(hobbits).element(1).isEqualTo(sam);
assertThat(hobbits).last().isEqualTo(pippin);
単一要素
assertThat(babySimpsons).singleElement()
.isEqualTo("Maggie");
特定フィールドの抽出
assertThat(users).extracting((u) -> tuple(u.id, u.rank)) .contains(tuple("abc", 10)) .contains(tuple("def", 20)); assertThat(users).extracting("id", "rank") .contains(tuple("abc", 10)) .contains(tuple("def", 20));
assertThat(params) .extracting(Param::key, Param::value) .containsExactlyInAnyOrder( tuple("analysisId", "abc"), tuple("projectId", "cde"));
assertAll (全て実行する)
失敗するものがあっても assertAll 内は全て検証される(ただし、結果が少々見にくい)
assertThat(persons.get(0)) .satisfies(p -> assertAll( () -> assertThat(p.getName()).isEqualTo("SYM"), () -> assertThat(p.getAge()).isEqualTo(0), () -> assertThat(p.getJob()).isEqualTo("Engineer") ));
例外
- 例外確認
assertThatThrownBy(() -> { throw new Exception("boom!"); }) .isInstanceOf(Exception.class) // 継承クラス含む .isExactlyInstanceOf(IOException.class) // 一致 .hasMessageContaining("boom");
- 例外が発生しない
// どちらも同じ assertThatNoException().isThrowBy(() -> System.out.println("")); assertThatCode(() -> System.out.println("OK")).doesNotThrowAnyException();
- 指定した例外発生確認
assertThatExceptionOfType(IOException.class) .isThrownBy(() -> { throw new IOException("boom!"); }) .withMessage("%s!", "boom") .withMessageContaining("boom") .withNoCause();
- 特定の例外確認
- assertThatNullPointerException
- assertThatIllegalArgumentException
- assertThatIllegalStateException
- assertThatIOException
assertThatIOException().isThrownBy(() -> { throw new IOException("boom!"); }) .withMessage("%s!", "boom") .withMessageContaining("boom") .withNoCause();
カスタムAssertion
var fooObj = new FooClass(); // 略 assertThat(fooObj).hasValue("bar"); public class FooClassAssert extends AbstractAssert<FooClassAssert, FooClass> { public FooClassAssert(FooClass actual) { // 定型文 super(actual, FooClassAssert.class); } public static FooClassAssert assertThat(FooClass actual) { // 定型文 return new FooClassAssert(actual); } public FooClassAssert hasValue(String key) { // 独自Assertion isNotNull(); if (actual.getValue(key) == null) { failWithMessage("エラーメッセージ"); } return this; // メソッドチェーンのために必須 } }
Mockito
- mock 全体をモック化
var mockedService = new Mock(MessageService.class); // モック when(mockedService).getMessage(any()).thenReturn("モック化"); doReturn("モック化").when(mockedService).getMessage(any()); // 呼び出し回数検証 verify(mockedService, times(2)).getMessage(any());
- spy 一部のみをモック化
var mockedService = new Spy(MessageService.class); doReturn("モック化").when(mockedService).getMessage(any());
※テスト対象クラスをspyするのは避けた方が良い。責務が大きくなり図来ている兆候 (シングルトンクラスのメソッドのテスト無理やりできるが非推奨。場合による)
- その他
mockito でコンストラクターの mock を使ったテストをしたい (Mockito 3.5.0 以降)
ref
以下も参考になりそう
コードレビュー どう取り組むか
コードレビュー どう取り組むか
言葉遣いで心掛けていること
- 上から目線、命令形は、ダメ絶対
- なぜ悪いコードなのか、なぜそう書いた方がいいかまで説明を付ける
- その上で、○○しませんか?(提案型)、or 明確にそうした方が良い時は○○しましょう(推奨型) でコメントを付ける。
観点
- 周辺との統一感が取れているか?
- ネストは浅くできているか?
- 失敗ケースは早く返す(単純な if else は if のみに。ループは continue)
- メソッド化してコードを文書化できているか?
- 長い条件式/文になっていないか?
- 見やすく分割されているか?
- 長い条件式/文になっていないか?
- 変数/関数/クラス/パッケージのスコープは小さいか?
- 小さくすることで依存を小さくできる=最小限にできているか?
- イミュータブルで済む物はそうできているか?
- 重複コード/冗長なコードがないか?
- ただし、意味合いが異なる物をひとまとめにしてはならない
- コードは適切に分割できているか?
- 汎用コードを(プロジェクトコードから)分離できているか?
- クラス/メソッド/関数は責務が一意か?明確か?
- 状態を持つ処理はクラス化できているか?
- コメント(Why?)の内容は具体的に(できれば具体例を添える)
- 処理への要約コメントを書く位ならメソッド化してコードを文書化する
- 仕様面
- 仕様を満たしているか
- 仕様に考慮漏れがないか
- デグレはないか
- 良い設計か
簡易なもののサンプル
- before
Class ExampleService { // シングルトン想定 private static ExampleService instance = new ExampleService(); // パス情報は別クラスで管理されている前提 private static PATH EXTERNAL_CONF_DIR_PATH = ProductPorperties.getConfDir(); private static EXTERNAL_XXX_CONF_NAME = "external" + File.separator + "external_xxx.conf"; private static EXTERNAL_YYY_CONF_NAME = "external" + File.separator + "external_yyy.conf"; private static EXTERNAL_ZZZ_CONF_NAME = "external" + File.separator + "external_zzz.conf"; private static Logger LOGGER = LoggerFactory.getLogger(ExampleService.class); private ExampleService() { initSetting() Path externalXxxConfPath = EXTERNAL_CONF_DIR_PATH.resolve(EXTERNAL_XXX_CONF_NAME); Path externalYyyConfPath = EXTERNAL_CONF_DIR_PATH.resolve(EXTERNAL_YYY_CONF_NAME); Path externalZzzConfPath = EXTERNAL_CONF_DIR_PATH.resolve(EXTERNAL_ZZZ_CONF_NAME); List<Path> configPaths = List.of(externalXxxConfPath, externalYyyConfPath, externalZzzConfPath); boolean isAllExist = true; for (Path p : configPaths) { if (Files.notExists(p)) { isAllExist = false; break; } } if (isAllExist) { try { loadExternalConfig() } catch (IOException e) { LOGGER.error("load failure.", e) } } else { LOGGER.inf("files non exist: " + Stirng.join(nonExistConfigs, ", ")); } } }
- after
Class ExampleService { // パス情報は別クラスで管理されている前提 private static String EXTERNAL_CONF_DIR_PATH = ProductPorperties.getConfDir() + File.separator + "external"; private static EXTERNAL_XXX_CONF_NAME = "external_xxx.conf"; private static EXTERNAL_YYY_CONF_NAME = "external_yyy.conf"; private static EXTERNAL_ZZZ_CONF_NAME = "external_zzz.conf"; Logger LOGGER = LoggerFactory.getLogger(ExampleService.class); ExampleService() { initSetting(); loadExternalConfigIfExist(); } void loadExternalConfigExist() { List<String> nonExistConfigNames = findExternalCoinf(); if (nonExistConfigs.isEmpty()) { LOGGER.inf("files non exist: " + Stirng.join(nonExistConfigs, ", ")); return; } try { loadExternalConfigs() } catch (IOException e) { LOGGER.error("load failure.", e) } } List<String> findExternalCoinf() { List<String> externalConfigNames = List .of(EXTERNAL_XXX_CONF_NAME, EXTERNAL_YYY_CONF_NAME, EXTERNAL_ZZZ_CONF_NAME); externalConfigNames.stream() .map(EXTERNAL_CONF_DIR_PATH::resolve) .filter(Files::notExists) .map(File::getFileName) .collect(Collections::toList); } }
命名まとめ
※随時追加
- ~ならxxxする: loadConfigIfExist
コードレビューでやること
最低限の品質担保(これが満たせないと本来レビューすべきことに中々入れない)
- コードフォーマット
- メソッドの長さ/ネストの深さなど
- 記述が分かりやすいか(メソッド名/関数名/簡潔なロジック)
- セキュリティ担保
- validateする
- 自前でHTML組立て->XSS、ユーザ入力そのままSQLに->SQLインジェクション、等
- パフォーマンス担保
- 不要なインスタンス
- ループ数削減できない?
- N+1問題
→ツールで機械的チェックできるものはそうした方が良い。良いフレームワークに乗っかることで回避できるものもある。
本来レビューすべきこと
- 仕様を満たしているか
- 仕様に考慮漏れがないか
- デグレはないか
- 良い設計か
→ 確認するためのフォーマットを設ける(以下実際に使っている物)
- 概要/目的
- この修正でできるようになること。
- 元ネタがあればリンクを貼る。
- 方針
- 実機確認結果
- 補足事項
- diffの当該行に直接コメント
※必要なら、方針の段階でレビューする (WIP) -> 手戻り防止
コードレビュー使い捨てにしないためには
経緯&対策まとめ:
- ミスや基本的な指摘が多すぎる。故に(レビューワー(=自分)疲弊で)すり抜けも発生…Why?
- 他の人が同じミスをする
- -> チーム内で共有 (他PR/MRにも共通するならそこにも書きにいく)
- 懇切丁寧に理由もセットで説明しても、同じ人でも同じミスを繰り返す
- -> これはどうすれいい…
- 基本的なところ(読みやすいコードの書き方)は勉強会実施(済)
- 勉強会やっても身に付かない説あるため、wikiに書いて都度見る文化?を作らないと恐らく浸透しない
- 他の人が同じミスをする
- ミス多いため、Sprint(1sprint/2週間)の境目で全体にレビュー指摘まとめて共有するようにしたが
- 各々に知識として身についてない説
- -> 物量がそこそこ多いので一気にやっても身にならない。絞るべき。
- (他者が指摘まとめ役を引き受けたが)時間ないでSkipされた…
- -> 役割放棄とみなして奪い取るしかない。絞って5分~10分で短くする(デイリーでできるように)
- -> コードへの理解があり重要度の切り分けができる人(自分…)がまとめる/取捨選択するしかない
- まとめたものがそもそも捨てられていて意味を成していない…
- -> 取捨選択してwikiに書き貯めよう
- まとめる労力は少なくない
- 各々に知識として身についてない説
Next:
- 書いても読まれないというのは見かける…が、それでも明文化は必要か
- レビュー指摘を取捨選択し、5~10分で「デイリー」で共有&wikiに書き貯める。(共通事項や製品ごとに分けた方が良さそう)-> レビュー出す前にこれを見るという文化?を形成して浸透させる
- コーディング力低い集団の中では、できる人が基本的な所から明文化する他なさそう(できる人高負荷問題加速…)
以下参考になりそう(明文化大事そう。↓ のように図示化できると良さそうだがどうやればよいか…)
ref
Gson Serialize (Object -> String) での時刻フォーマット指定方法
Gson Serialize (Object -> String) での時刻フォーマット指定方法
速攻で解決したが念の為のメモ。
基本
Gson: https://github.com/google/gson ※Tutorialへのリンク有り
- serialize (obj -> json)
Gson gson = new Gson();
String json = gson.toJson(obj);
- deserialize (json -> obj)
String json = "{ ~ }"; Gson gson = new Gson(); SampleClass obj = gson.fromJson(json, SampleClass.class);
時刻フォーマット指定方法
- Date -> OffsetDateTime形式(文字列) の例
GsonBuilder に registerTypeAdapter で登録すればよい。Deserializerも同様。
Objectの中にObjectがあり、そこにDateがある場合でも、以下で可能
class SampleComverter<T> { public static String convertObjToJson(T obj) { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(Date.class, new DateSerializer()); Gson gson = gsonBuilder.setPrettyPrinting().create(); return gson.toJson(obj); } private static class DateSerializer implements JsonSerializer<Date> { @Override public JsonElement serialize(Date src, Type srcType, JsonSerializationContext context) { OffsetDateTime datetime = OffsetDateTime.ofInstant(src.toInstant(), ZoneOffset.UTC); return new JsonPrimitive(DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(datetime)); } } }
ref
以下が分かりやすかった。
公式チュートリアルだと、以下の章に対応する話。