SYM's Tech Knowledge Index & Creation Records

「INPUT:OUTPUT=1:1以上」を掲げ構築する Tech Knowledge Stack and Index. by SYM@設計者足るため孤軍奮闘する IT Engineer.

Golang Gorm 実践メモ&注意点

Golang Gorm 実践メモ&注意点

随時追記

公式ドキュメントhttps://gorm.io/ja_JP/docs/index.html

注意事項

  1. Create、Update、Save の振舞いの違い

  2. Create: 新規作成

  3. Update: 空値が除外、任意のカラムのみ選択可能
  4. Save: 空値も含めて全てのカラムを一律保存

使い分けの方針

  • 新規レコード作成時: Create
  • 既存レコード更新時: Update
  • 更新時に空値を含めてStructで更新: Save
  • 更新時に空値を含めてMapで更新: Update

Saveは、なければ新規作成、あれば更新。となるため注意が必要

  1. (コールバックで)自動的に関連レコードが全て保存される

関連レコード含めて全て更新される

回避: 関連レコードの一括保存や、そのほかBeforeCreateのようなコールバックメソッドを呼ばずに更新したい場合、 UpdateColumn、UpdateColumnsを使用

  1. マイグレーション機能は本番向きでない

一回きりのマイグレーションで完結する場合は十分かもしれないが、長期的な運用の管理には機能不足

migrate 等、別のツールを使う

  1. 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

Repository は集約の単位で作成

ref:

集約

  • 永続化の単位となるクラス群。境界の定義
  • トランザクション範囲。(トランザクション整合性)
  • 集約の操作は集約ルートを介してのみ可能
  • 集約は小さくし、トランザクションの範囲は最小限にする。->トランザクションの衝突が少なく、スケーラビリティ/性能が良く保てる
  • 別の集約ルートは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

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

entityからコード自動生成した話

Elasticsearch メモ

Elasticsearch メモ

概要

  • Elastic Stack の 1製品
  • JSONベースの高速検索を可能とする検索・分析エンジン

用途: ブログ/記事投稿サイト等のWebサービス全文検索+α?に使われている模様

メモ: - RDBに被せて使える物でもなく、利用するなら併用する形式 - RDBとは別サーバリソースが必要 - RDBのデータをElasticsearchに同期的に投入する必要がある

RDBの代替にはならない

Elasticsearchのデータストアとしての振舞いの特徴

大量の文書を高速に検索することに適した仕組みを活かして、限定的に使用するのが良い

  • RDBと比べてのデメリット

Elasticsearchは速度改善だけで選ぶものではない

  • Elasticsearchにマッチする要件
    • 自然言語で記述された大量の文書データに対し、より文章として自然な検索結果を得たい
    • 検索キーワードを基にあいまいな検索結果を得たい
    • 肥大化するデータに対し、動的にクラスタ構成をスケーリングしたい

上記はRDBでは対応が難しい場合が多く、文章の検索に特化したElasticsearchならではの活用範囲

ref

Elasticsearch 入門。その1

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();
assertThat(bar1).isSameAs(bar2);
assertThat(bar1).isNotSameAs(bar2);
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

メソッド名、迷った時に参考にできる単語一覧

コードレビューでやること

最低限の品質担保(これが満たせないと本来レビューすべきことに中々入れない)

  • コードフォーマット
  • メソッドの長さ/ネストの深さなど
  • 記述が分かりやすいか(メソッド名/関数名/簡潔なロジック)
  • セキュリティ担保
  • パフォーマンス担保

→ツールで機械的チェックできるものはそうした方が良い。良いフレームワークに乗っかることで回避できるものもある。

本来レビューすべきこと

  • 仕様を満たしているか
  • 仕様に考慮漏れがないか
  • デグレはないか
  • 良い設計か

→ 確認するためのフォーマットを設ける(以下実際に使っている物)

  • 概要/目的
    • この修正でできるようになること。
    • 元ネタがあればリンクを貼る。
  • 方針
  • 実機確認結果
  • 補足事項
    • diffの当該行に直接コメント

※必要なら、方針の段階でレビューする (WIP) -> 手戻り防止

コードレビュー使い捨てにしないためには

経緯&対策まとめ:

  • ミスや基本的な指摘が多すぎる。故に(レビューワー(=自分)疲弊で)すり抜けも発生…Why?
    • 他の人が同じミスをする
      • -> チーム内で共有 (他PR/MRにも共通するならそこにも書きにいく)
    • 懇切丁寧に理由もセットで説明しても、同じ人でも同じミスを繰り返す
      • -> これはどうすれいい…
    • 基本的なところ(読みやすいコードの書き方)は勉強会実施(済)
      • 勉強会やっても身に付かない説あるため、wikiに書いて都度見る文化?を作らないと恐らく浸透しない
  • ミス多いため、Sprint(1sprint/2週間)の境目で全体にレビュー指摘まとめて共有するようにしたが
    • 各々に知識として身についてない説
      • -> 物量がそこそこ多いので一気にやっても身にならない。絞るべき。
    • (他者が指摘まとめ役を引き受けたが)時間ないでSkipされた…
      • -> 役割放棄とみなして奪い取るしかない。絞って5分~10分で短くする(デイリーでできるように)
      • -> コードへの理解があり重要度の切り分けができる人(自分…)がまとめる/取捨選択するしかない
    • まとめたものがそもそも捨てられていて意味を成していない…
      • -> 取捨選択してwikiに書き貯めよう
    • まとめる労力は少なくない
      • -> GithubならAPIで収集すればお手軽にできる。(自作ツールを作った)
      • -> Gitlabの場合、手で拾わざるを得ない(レビューコメントへのリンクやdiffがAPIで拾えない)。お手上げ…

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

以下が分かりやすかった。

公式チュートリアルだと、以下の章に対応する話。