Gobble up pudding

プログラミングの記事がメインのブログです。

MENU

JavaScriptでコールバックで失敗を検知した時にどうやってリトライするか

f:id:fa11enprince:20200729013245j:plain Node.jsの例えばfs.renameのような非同期のメソッドを使っていたときなどに、
リトライしようとして、それをどうやって実現するかという話です。
コールバックだと単純にはうまくいきません。
いろいろ方法はあると思いますが、一例を説明しようと思います。
Node.jsのv12以降なら動くと思います。

サンプルの関数

気軽に実行するには
https://repl.it/
とかのNode.jsに貼り付けて実行してもらえればと思います。
例えば次のような関数があったとします。fsモジュールとかfsExtraモジュールの非同期系メソッドだと思ってください。 同期系はわけあって使いたくないです。
awaitを使うのと同期系のメソッドを単に使うのとでは次のように違いがあります。

asyncというキーワードをメソッドに宣言した上で、awaitというキーワードを使うことで手続き型らしくも非同期的な処理を記述することが出来ます。注意してほしい点としては、あくまでも開発者が同期的に書き下すことができるだけであって、つまりはコンパイラ(インタプリタ)がよしなに非同期に書き換えているということです。 非同期処理を理解する - Sansan Builders Blog

async function dummyMove(src, dst, option, cb) {
    console.log(`src: ${src}`);
    console.log(`dst: ${dst}`);
    console.log(option);
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(cb('error occurred')), 500)
    });
}

なにやら引数を受け取って、500ミリ秒経過後に非同期で必ずエラーを返す関数です。 今回の例ではPromiseを使っているのですが、async functionにしています。
本来はPromiseを返す場合はasyncにする必要がないのですが、特に無害なのでasyncにします。 呼び出し方はこんな感じです。

第一形態

(() => {
    dummyMove("aaa1.txt", "bbb1.txt", { overwrite: true }, (err) => {
        if (err) {
            console.log(`error occurred. src: aaa1.txt, dst: bbb1.txt, error: ${err}`);
        } else {
            console.log(`succeeded. src: aaa1.txt, dst:  bbb1.txt`);
        }
    });
})();

さて、ここでこのdummyMoveを複数回呼び出す必要があります。
しかもわけあって、一つ目が終わったら二つ目というように処理をしないといけません。

第二形態

じゃあ、関数化するか…。で、こいつはコールバックを使ってるからPromiseもしくはasyncにしてやればいいねってなります。

async function moveWithRetry(src, dst) {
    return new Promise((resolve, reject) => {
        dummyMove(src, dst, { overwrite: true }, async (err) => {
            if (err) {
                console.log(`error occurred. src: ${src}, dst: ${dst}, error: ${err}`);
                reject(`failed: src: ${src}, dst: ${dst}, error: ${err}`);
            } else {
                console.log(`succeeded. src: ${src}, dst: ${dst}`);
                resolve(null);
            }
        });
    });
}

(async () => {
    await moveWithRetry("aaa1.txt", "bbb1.txt").catch(() => console.log('e1'));
    await moveWithRetry("aaa2.txt", "bbb2.txt").catch(() => console.log('e2'));
})();

ちなみにですが、さっきのケースとは異なり、
今回のルールでは二つ目は一つ目が終わってからでないとダメというルールがあるのでawaitをつけてあげます。
つけないとどうなるかは外して何度か実行してみるとわかると思います。パラレルに実行される感じになります。
さらに、これだとエラーかどうかにかかわらず、必ず一つ目も二つ目も実行されます。 こう書いた場合、

    try {
        await moveWithRetry("aaa1.txt", "bbb1.txt");
        await moveWithRetry("aaa2.txt", "bbb2.txt");
    } catch(e) {
        console.log('e');
    }

この場合は当然一つ目で失敗すると二つ目は呼ばれません。 どちらが適切かはケースバイケースだと思います。

第三形態

さて、ここで、困ったことが起きました。
なんか知らないけれど、(httpとかの例にすればよかったけれど)たまーに失敗することがあるそうです。
ということでリトライ処理を入れましょう。
ああ、簡単。と思って…同期処理に慣れている人が書くと事故ります(僕です)。 ダメな例

async function moveWithRetry(src, dst, retryCnt = 0, maxRetry = 5) {
    return new Promise((resolve, reject) => {
        console.log(`retryCnt: ${retryCnt} / maxRetry: ${maxRetry}`);
        while (retryCnt < maxRetry) {
            dummyMove(src, dst, { overwrite: true }, async (err) => {
                if (err) {
                    console.log(`error occurred. src: ${src}, dst: ${dst}, error: ${err}`);
                    reject(`failed: src: ${src}, dst: ${dst}, error: ${err}`);
                    retryCnt++;
                } else {
                    console.log(`succeeded. src: ${src}, dst: ${dst}`);
                    resolve(null);
                }
            });
        }
    });
}

(async () => {
    await moveWithRetry("aaa1.txt", "bbb1.txt").catch(() => console.log('e1'));
    await moveWithRetry("aaa2.txt", "bbb2.txt").catch(() => console.log('e2'));
})();

この場合、どうなるかというと、ほぼ無限ループ状態になってしまいます。
というのも、コールバック内でカウントしても、コールバックが呼ばれる前にdummyMoveをものすごい速さで呼ぶので、意図した動きになりません。
ということで、エラーの時はretryCntを超えていなければ、再帰的にmoveWithRetryを呼んであげればいい感じになります。

async function moveWithRetry(src, dst, retryCnt = 0, maxRetry = 5) {
    return new Promise((resolve, reject) => {
        if (retryCnt >= maxRetry) {
            console.log('retryCnt exceeded!');
            reject(`failed: src: ${src}, dst: ${dst}`);
        }
        dummyMove(src, dst, { overwrite: true }, async (err) => {
            if (err) {
                console.log(`error occurred. src: ${src}, dst: ${dst}, error: ${err}`);
                console.log(`retryCnt: ${retryCnt}`);
                if (retryCnt < maxRetry) {
                    console.log('retry!');
                    await moveWithRetry(src, dst, ++retryCnt).catch((e) => reject(e));
                } else {
                    reject(`failed: src: ${src}, dst: ${dst}, error: ${err}`);
                }
            }
            else {
                console.log(`succeeded. src: ${src}, dst: ${dst}`);
                resolve(null);
            }
        });
    });
}

(async () => {
    await moveWithRetry("aaa1.txt", "bbb1.txt").catch(() => console.log('e1'));
    await moveWithRetry("aaa2.txt", "bbb2.txt").catch(() => console.log('e2'));
})();

こうすれば意図通りです。 まぁそんなわけでPromiseを返してくれない旧来のAPIはちょっとこういうことをしようと思うと、妙に苦労します…。

補足

なんかUnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch().とか出るんですけどって思って書いたときのコードがコレ。
しかも二つ目が実行されないし。

async function moveWithRetry(src, dst, retryCnt = 0, maxRetry = 5) {
    return new Promise((resolve, reject) => {
        if (retryCnt >= maxRetry) {
            console.log('retryCnt exceeded!');
            reject(`failed: src: ${src}, dst: ${dst}`);
        }
        dummyMove(src, dst, { overwrite: true }, async (err) => {
            if (err) {
                console.log(`error occurred. src: ${src}, dst: ${dst}, error: ${err}`);
                console.log(`retryCnt: ${retryCnt}`);
                if (retryCnt < maxRetry) {
                    console.log('retry!');
                    await moveWithRetry(src, dst, ++retryCnt);
                } else {
                    reject(`failed: src: ${src}, dst: ${dst}, error: ${err}`);
                }
            }
            else {
                console.log(`succeeded. src: ${src}, dst: ${dst}`);
                resolve(null);
            }
        });
    });
}

ひとつ前のコードと比べてもらうとわかりますが、await moveWithRetry(src, dst, ++retryCnt);catchしていません。
なんかエラーがでたら落ち着いてコードを見直しましょう。

参考

https://qiita.com/G-awa/items/652107a9abf7ff6d0d06
https://qiita.com/hey1you1/items/a9b144c94f84cd1d91b8
https://ja.javascript.info/async-await
https://stackoverflow.com/questions/18581483/how-to-do-repeated-requests-until-one-succeeds-without-blocking-in-node
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await

C++でオブジェクトを返す関数を書いてもいいのか問題

f:id:fa11enprince:20200722012730j:plain
C++でオブジェクトを返す関数を書いてもいいのか問題→OKです。
遅かったらプロファイル取りましょうで終わりです。

かなり過去にC++でstd::stringをどう返すべきかという記事を書いたこともあり結局いま、例えば、関数内でvectorを加工してその結果を呼び出し側で使いたいニーズがある場合のコードはどう書くのが正解なの? ということを思い至り備忘録として残しておきます。
※話が複雑になるので、例えばsortするときのようにあらかじめあるvectorを加工する場合ではないことを明記しておきます

主にC++11が出る前のように

void foo(std::vector<int>& v) {
  ...(vector書き換え)
}
std::vector<int> vec = { 1, 2, 3 };
foo(vec);

とやるのがいいのか、

std::vector<int> foo() {
  std::vector<int> v = { 1, 2, 3 };
  ...(vector書き換え)
  return v;    // ここが気になる!
}
std::vector<int> vec = foo();

のがいいのかという話です。自信がなかったので調べてみました。

前者だと前時代的な感じだし、引数経由で結果を受け取るっていうのもアレだし、
参照だとそもそも呼び出し側で書き換えてますよってのがよくわかんない…
っていうのもあって微妙感ありますよね。
後者で書ければいいけど、可読性重視したけどコピーが発生して速度低下したら元も子もない。
さらに、後者の場合でstd::moveの必要があるのかどうなのか?ってあたりも気になりました。

結論

ググったらすぐわかることなのですが、 こことか ここに答えが書いてあります。

もはや現代では(あらかじめ何か値があるものを外から受け取って加工するわけでないこのケースでは)オブジェクトを返す関数を書くのが最適解という結論が出てしまいました。

検証コード

www.bit-hive.com
のコードを参考にRVOが効いているかどうか検証しました。

#include <iostream>
#include <vector>

// xにrbpレジスタの値を設定(gcc)
#define CURRENT_RBP(x) \
  __asm__("mov %%rbp, %0;" \
            :"=r"(x) \
          );

// RVOが適用されているかをチェックするマクロ
#define CHECK_RVO(tag, object) \
{ \
  unsigned long rbp; \
  CURRENT_RBP(rbp); \
  unsigned long address = reinterpret_cast<unsigned long>(&object); \
  std::cout << "rbp:     0x" << std::hex << rbp << std::endl; \
  std::cout << "address: 0x" << std::hex << address << std::endl; \
  if (rbp > address) { \
    std::cout << tag << ": RVO isn't applied." << std::endl; \
  } else { \
    std::cout << tag << ": RVO applied." << std::endl; \
  } \
}

class Something {
public:
  int value;
  Something() : value(0) {}
  Something(const Something& obj): value(obj.value) {
    std::cout << "Copy constructor called." << std::endl;
  }
  Something(const Something&& obj) noexcept: value(std::move(obj.value)) {
    std::cout << "Move Constructor called." << std::endl;
  }
};

// 名前付きのオブジェクトを返す関数
Something create_object() {
  Something object;
  CHECK_RVO("object", object);
  object.value = 1;
  return object;
}

// 名前付きのオブジェクトを返す関数 vector
std::vector<Something> create_objects() {
  std::vector<Something> objects;
  //std::vector<Something> objects {Something(), Something(), Something()}; だとコピーになる
  CHECK_RVO("objects", objects); 
  objects.emplace_back(Something());
  objects.emplace_back(Something());
  objects.emplace_back(Something());
  int i = 0;
  for (Something& s : objects) {
    s.value = ++i;
  }
  return objects;
}

int main() {
  Something object1 = create_object();
  std::cout << "Check result" << std::endl;
  std::cout << object1.value << std::endl;
  
  std::cout << "------" << std::endl;
  std::vector<Something> objects = create_objects();
  std::cout << "Check result" << std::endl;
  for (Something& s : objects) {
    std::cout << s.value << std::endl;
  }
}

実行結果

https://wandbox.org/permlink/3SHbmZ1DybX6nXB4

そもそもRVOってどうやってやってるんだという話

上記の検証コードを見ていただけるとわかりますが、
要は関数の戻り先の領域ににあらかじめ値を格納してやれば、
関数が終わってもその値がコピーせずに使える…というものらしいです。

上記のマクロ(CHECK_RVO)でチェックしているところはこんな感じです。
RBP(64bitスタックベースポインタレジスタ)は要は関数の始まりのアドレスです。
スタックはあとのが上に積まれるので(ただし、アドレスは小さい方向に成長していく)、
通常は、

+---------------------------+
|0x7fff1e32bc60 オブジェクト |
+---------------------------+
|0x7fff1e32bc4c 関数 RBP    |
+---------------------------+

となっているのだけれど、

+---------------------------+
|0x7fff1e32bc60 関数 RBP    |
+---------------------------+
|0x7fff1e32bc9c オブジェクト |
+---------------------------+

RVOが効いたときは、このようになっています。
これでコピーしなくてよい!めでたしめでたしとなるわけです。

自然なコードを書いてもよさそう。

例外的なケース

とはいえ、sortのときのように何かを渡して加工する場合を考えると…

std::vector<int> foo(std::vector<int> v) {
  ...(vector書き換え)
  return v;
}
std::vector<int> vec1 = { 1, 2, 3 };
std::vector<int> vec2 = foo(vec1);

としてしまうとセマンティクスはいいものの、引数に渡したときにコピーが発生します。 速度と引き換えにImmutableな関数にしたい場合はこんな感じになっちゃうのかなと。 そのときは引数にconstつけてあげたほうが良いですね。
vectorが巨大な配列だった場合は前者の参照渡しにするしかないのかもしれません。

void foo(std::vector<int>& v) {
  ...(vector書き換え)
}
std::vector<int> vec1 = { 1, 2, 3 };
foo(vec1);

速度を求める場合はこうするしかなさそう。これはほかの言語でも結局こういう書き方になりますね。

この辺の話はここにまとまっていました。 …残念ながらC++を仕事で使うのは当分なさそうですが(T_T)

関連する話題

ふと、ここで、昔からどうするのがいいか悩んでいるところなのだけれど、 下記はJavaですが、
1.参照を更新

private void updateList(List<A> listOfA) {
  listOfA.forEach(item -> { 
    item.setProperty1(findSomething());
  }
}

2.参照を更新したものをreturn

private List<A> updateList(List<A> listOfA) {
  listOfA.forEach(item -> { 
    item.setProperty1(findSomething());
  }

  return listOfA;
}

3.新たなリストをreturn

private List<A> updateList(List<A> listOfA) {
  List<A> newList = new ArrayList<>();
  // Logic to copy listOfA in newList....
  newList.forEach(item -> { 
    item.setProperty1(findSomething());
  }
  return newList ;
}

3は状況に応じて最適ってわかるんですが、問題は1がいいのか2がいいのか… 2が個人的にはよく使っていたのですが、2だとおいおい、渡したやつも書き換わってるのかよ!って突っ込まれそうです。
2のメリットは書き換わっていることが書き方的にわかりやすいのと戻り値に直接メソッドを適用できてメソッドチェーンができるといったところでしょうか。でも無難に1にしようかなと迷うところです。
参考: java - Update parameters of a list in a method: return list or not - Stack Overflow
あーいやいや、ちゃんとオブジェクト指向をしていれば引数にListが渡ってくることはない?っていう気もしなくもない

あとがき

ふと、こんなツイートをみたのが記事を書いたきっかけです。

ちょっとだけ(本当にちょっとだけ)最近どうなってるの?っていうのを見てみたら、何やら色々増えてます。
気になったのが、
値のコピー省略を保証 - cpprefjp C++日本語リファレンス
で、そういえばRVO周りどうなってるんだ?っていうのがそもそもの発端です。

Spring Boot 2 + JQuery DataTablesで一覧画面を作成

f:id:fa11enprince:20200709020731j:plain
今更ながらjQuery系ですが、使ったことがなくてでも便利そうなので使ってみました。
皆さん大好きSpring Boot 2を使用しました。
また、Pagingが面倒なので皆さん大嫌いHibernateを利用しています。
結論としてはJQuery DataTablesは思ったより使いやすくて便利だ!ということです。
もしかして、サーバーから全件読んでクライアント側でこねこねしないとだめなやつ?
と思いましたが、そんなことはなく、便利なやつです。柔軟性が高いです。
serverSide: trueオプションをつけるとちゃんとサーバから10件取って、ページャー押したら10件取って、
が簡単に実現できます。

できあがったもの

f:id:fa11enprince:20200722221444p:plain 画面は3画面作っていて、

  • 旧スタイルのサーバーサイドレンダリング(?)オンリーの画面
  • とりあえずのVueの画面
  • 今回本題のJQuery DataTablesを使用した画面

でできています。おまけ程度にログインもついています。

動作に必要なもの

Maria DB
bash (Git Bashとか)
Java 8
あとはそもそもGitにあがっているか、pom.xmlちゃんが引っ張ってきてくれます。

実行方法

EclipseでMaven installをして実行します。この場合Lombokが必要になります。
もしくは…

cd my-address-search
bash mvnw install
java -jar target/my-address-search-0.0.1-SNAPSHOT.jar

とかで実行してhttp://localhost:8080/でOKと思います。

編集、削除用のエリアは設けたものの見てお分かりの通り実装していないです。

ところどころ手を抜いているところはあるのであくまでサンプルとしてご容赦いただければ。
データはある程度多めのリアルのデータが良いということで、
郵便番号データを使用しています。
ただし、日本郵便で公開しているものは問題が多いCSVなので、
ここで公開させてくださっているありがたいデータを使っています。

あと細かいところはGitHub上のREADME.mdを見ていただければと思います。

JQuery DataTableとSpring Bootの組み合わせの解説編

近日追記する予定です。わかる人はコードを見るとわかるかと…。

ハマった内容

応答が受け取れない

success: function(json) {
}

で受け取れない。
てか、なんとも言わず…ダメな感じになる。
こういう感じで何かサーバーのレスポンスをcallbackでごにょごにょしたいときは

dataSrc: function(json) {
}

というJQuery DataTableの専用のメソッドを使いましょう。
というか公式ドキュメント読まないとだめですね。
割と丁寧なドキュメントなので読みましょう。

Spring側でリクエストをマッピングできない

List<T>型を受け取れないのに、受け取れないということがありました。
具体的にはJavaScript側でソート内容を作ってサーバーに送り付けるのに、

var sortDatas = [];
for (var i = 0; i < data.order.length; i++) {
    var index = data.order[i].column;
    var direction = data.order[i].dir;
    var columnName = data.columns[index].data;
    var sd = {
        "direction": direction,
        "property": columnName,
    };
    sortDatas.push(sd);
}
...
orders: sortDatas, ...

のようなことをやって、

"orders": [{"direction": "asc", "property":"city"}, {"direction": "asc", "property":"district"}]

のようなソート順をサーバー側に渡したときに、JQuery DataTablesが
orders[0][property] = "city", orders[0][direction] = "asc"
としてしまいSpringのControllerの引数(リクエストパラメータ)処理側でどうにもならない問題です。
むりやりJacksonでCustom Json Desirializer的なものを書くのか??うーんと悩んでいましたが、
そもそもJQuery DataTablesが行っているSerializeを変えてやればいいというお話でした。
orders[0][property] = "city"orders[0].property = "city"に変えてやればSpring側で解釈できます。
そのために使っているのが、jquery.spring-friendly.jsです。

ここで気づいたのですが、そもそも便利なものがやはりあるのですね。 今回は自力で頑張っていますが、これを使えば一挙解決です。

Springでのエラーハンドリング

DataTables版では以前紹介した例外ハンドラ@RestControllerAdviceを使っています。
検索時の変な文字を突っ込んだ時にエラーを一応気持ち程度表示してます。

参考

DataTablesの使い方 - Qiita
Server-side processing

Spring Bootでマルチデータソースのやり方

f:id:fa11enprince:20200613105140j:plain 以前このブログで紹介したMaven + Eclipseでマルチモジュールプロジェクトを作成するのなかでサンプルコードで示したものの特にマルチデータソースのやり方についての解説です。

この記事に書いてある通り、PostgreSQLとMongoDBを組み合わせる場合を例として書いてあります。

完成版のソースコードはこちらです。

ミドルウェア バージョン
Spring Boot 2
Java 1.8
Apache Maven 3

細かいバージョンはGitHubを参照してください。

Spring-Data-JPAとSpring-Data-MongoDBを利用した場合のやり方を解説します。

構成の概要

プロジェクト/モジュール/パッケージは次のようになっています。
2モジュールありmulti-module-batchはサンプルを動かす利用側コード
multi-module-commonはデータアクセス系を提供する共通モジュールの位置づけです。
f:id:fa11enprince:20200613104405p:plain

設定ファイル

PostgreSQLとMongoDBを利用する場合を考えてみます。
まずは設定ファイルの構成を考えます。
application.yml

spring:
  datasource:
    postgres:
      url: jdbc:postgresql://localhost:5433/postgres
      username: postgres
      password: postgres
    mongo:
      host: localhost
      port: 27018
      database: mongo
  profiles:
    include: common

重要なところのみ抜粋です。
このように利用する側の設定で、ユーザ名、パスワード、データベースを指定できれば設定ファイルとしては十分かと思います。
また、このプロジェクトではマルチモジュールを採用していてデータベースアクセス部分はcommonのプロジェクトが別にいます。
例えばコネクションプール系の設定はだいたい一緒でいいだろうというのがあると思いますので、
commonの設定に切り出したいとします。
それがinclude: commonの部分です。この場合Springがapplication-common.ymlという設定ファイルを追加で読み込みます。

共通モジュール側のプロジェクトのapplication-common.yaml

packages:
  common:
    component: com.example.multiModule
    postgres:
      entity: com.example.multiModule.common.spring.postgres.entities
      repository: com.example.multiModule.common.spring.postgres.repositories
    mongo:
      entity: com.example.multiModule.common.spring.mongo.entities
      repository: com.example.multiModule.common.spring.mongo.repositories

spring:
  datasource:
    postgres:
      driver-class-name: org.postgresql.Driver
    hikari:
      minimum-idle: 2 # Default: Same as maximum-pool-size
      maximum-pool-size: 4 # Default: 10
      idle-timeout: 60000 # Default: 60seconds
      connection-timeout: 30000 # Default: 30000
      leak-detection-threshold: 5000 #5sec Default: 0 -> disable
      max-lifetime: 600000 # 10min, Default: 1800000 (30 minutes)

使用するドライバとコネクションプールの設定です。
Mongoは今回特にこれに相当する設定をしないので、省略しています。
packagesの設定は@EnableJpaRepositories, @EnableMongoRepositoriesで利用します。

マルチデータソースの場合、Configを個別に用意してあげる必要があります。

マルチデータソースの場合、Configを個別に用意してあげる必要があります。
こちらのプロジェクトではデータアクセスにかかわる部分、RepositoryとEntittyも実装します。
また、Serviceも今回こちらに実装します。
基本的に定型の設定をコードベースで書くことになります。ある意味覚えゲーです。

PostgreSQL設定

ポイントとなるのはPostgresConfig.javaMongoConfig.javaです。

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
    basePackages = {"${packages.common.postgres.repository}"},
    entityManagerFactoryRef = "postgresEntityManagerFactory",
    transactionManagerRef = "postgresTransactionManager"
)
@EntityScan(basePackages = { "${packages.common.postgres.entity}" })
public class PostgresConfig {

    @Autowired
    PackageConfig packageConfig;

    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.postgres")
    public DataSourceProperties postgresDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @Primary
    @ConfigurationProperties("spring.jpa.postgres")
    public JpaProperties postgresJpaProperties() {
        return new JpaProperties();
    }

    @Bean(name = "postgresDataSource")
    @ConfigurationProperties("spring.datasource.hikari")
    public HikariDataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    @Bean(name = "postgresEntityManagerFactory")
    @Primary
    public LocalContainerEntityManagerFactoryBean postgresEntityManagerFactory(
            final EntityManagerFactoryBuilder builder,
            @Qualifier("postgresDataSource") final DataSource dataSource) {
        return builder
                .dataSource(dataSource)
                .packages(packageConfig.getCommon().getPostgres().getEntity())
                .persistenceUnit("postgres")
                .properties(postgresJpaProperties().getProperties())
                .build();
    }

    @Bean(name = "postgresTransactionManager")
    @Primary
    public PlatformTransactionManager postgresTransactionManager(
            @Qualifier("postgresEntityManagerFactory") final EntityManagerFactory entityManagerFactory) {
        final JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }
}

カスタムで設定を書くので@EnableTransactionManagement, @EnableJpaRepositoriesの指定は必須となります。
後者の@EnableJpaRepositoryですが@Repositoryをついたクラスを検索してBeanとして登録するためのものです。
また、マルチモジュールにしている関係でEntityの場所がどこ?となってしまうので@EntityScanを指定してやる必要があります。

各メソッドの説明

設定値を読む用のメソッドもあったりして厄介ですが、このクラスのメソッドで絶対に書かなければならないのが、3つあります。

    @Bean(name = "postgresDataSource")
    @ConfigurationProperties("spring.datasource.hikari")
    public HikariDataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

これが1つ目です。HikariCPを使うようにしています。
さらにDataSourceProperties経由でjdbcドライバ等の設定を読むようにしています。
名前が被るのでBean名をアノテーションで変更してあげます。ここではpostgresDataSourceとしています。
細かい内容はこの辺りの内容が参考になると思います。
https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-data-access

2つ目が

    @Bean(name = "postgresEntityManagerFactory")
    @Primary
    public LocalContainerEntityManagerFactoryBean postgresEntityManagerFactory(
            final EntityManagerFactoryBuilder builder,
            @Qualifier("postgresDataSource") final DataSource dataSource) {
        return builder
                .dataSource(dataSource)
                .packages(packageConfig.getCommon().getPostgres().getEntity())
                .persistenceUnit("postgres")
                .properties(postgresJpaProperties().getProperties())
                .build();
    }

です。なんとも長ったらしいメソッドですが、
LocalContainerEntityManagerFactoryBeanを返すメソッドを作ってやらなければなりません。
エンティティ登録管理のファクトリを教えてくれってことですね。Springムズカシイ。
補足ですがアノテーションの@QualifierはどのDataSourceよってSpringがわかんないので、
さっき定義したpostgresDataSouceっすよってことで教えてあげます。
しばらく触ってないと忘れがちですが@Beanによってクラスを返すメソッドを定義してSpring Beanにすると @Autowiredで注入できるのですよね。
何を言っているのかわからねー、という人はここがわかりやすいかなと思います。

紛らわしいのが単にgetter/setterの塊のJava Beans。これとは違う用語なので混同しないでくださいね。

3つ目、

    @Bean(name = "postgresTransactionManager")
    @Primary
    public PlatformTransactionManager postgresTransactionManager(
            @Qualifier("postgresEntityManagerFactory") final EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }

これも先ほど同様にな感じで先ほど定義したBeanを注入して設定する、な感じです。

MongoDBの設定

Spring-Data-MongoDBを利用する前提です。
こちらはずいぶんシンプルです。設定の読み込みをのぞけば2メソッド書けば終わりです。
また、@EnableMongoRepositories@EntityScanを書く必要があります。後者はマルチモジュールにしているためですね。

@Configuration
@EnableMongoRepositories(basePackages = { "${packages.common.mongo.repository}" })
@EntityScan(basePackages = { "${packages.common.mongo.entity}" })
public class MongoConfig {
    
    @Autowired
    MongoMappingContext mongoMappingContext;
    
    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.mongo")
    public MongoProperties mongoProperties() {
        return new MongoProperties();
    }

    @Bean
    @Primary
    public MongoClient mongo() {
        return new MongoClient(mongoProperties().getHost(), mongoProperties().getPort());
    }

    @Bean
    @Primary
    public MongoTemplate mongoTemplate() throws Exception {
        MongoDbFactory factory = new SimpleMongoDbFactory(mongo(), mongoProperties().getDatabase());
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
        converter.setTypeMapper(new DefaultMongoTypeMapper(null));  // remove _class
        
        return new MongoTemplate(factory, converter);
    }
}

各メソッドの説明

    @Bean
    @Primary
    public MongoClient mongo() {
        return new MongoClient(mongoProperties().getHost(), mongoProperties().getPort());
    }

解説不要そうなこちらと

    @Bean
    @Primary
    public MongoTemplate mongoTemplate() throws Exception {
        MongoDbFactory factory = new SimpleMongoDbFactory(mongo(), mongoProperties().getDatabase());
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
        converter.setTypeMapper(new DefaultMongoTypeMapper(null));  // remove _class
        
        return new MongoTemplate(factory, converter);
    }

こちらが必要です。実はこちらについては

public MongoTemplate mongoTemplate() throws Exception {
    return new MongoTemplate(
              new SimpleMongoDbFactory(new MongoClient(mongo.getHost(), mongo.getPort()), mongo.getDatabase()));
}

と書くだけでよいのですが、デフォルトのまま使うとすべてのコレクションのドキュメントに_classと勝手にフィールドがつくのでそれが嫌だったので消す設定を追加しています。
以上で終わりです。

どう使えばいいのか

それで、このように定義した場合後はどうすればいいかというとEntityとRepositoryの置き場さえ守れば、
通常通りEntityとRepositoryを書けます。
PostgreSQLの場合、@EnableJpaRepositoriesと設定ファイルで、
Entity: com.example.multiModule.common.spring.postgres.entities
Repository: com.example.multiModule.common.spring.postgres.repositories
とパッケージを指定してあげてます。
Mongoの場合もほぼ同様で、
Entity: com.example.multiModule.common.spring.mongo.entities
Repository: com.example.multiModule.common.spring.mongo.repositories
つまり、PostgreSQLはpostgesパッケージ配下、MongoDBはmongoパッケージ配下に書けばよいことになります。

詳細はGitHubのリンクを参照してください。

備考

multi-module-batchにある利用する側のモジュールのコードです。

@SpringBootApplication
@ComponentScan(basePackages = { "${packages.common.component}", "${app.packages}" })
public class SampleDbAccessApp implements ApplicationRunner {
    @Autowired
    PosgresSampleService postgresSampleService;
    @Autowired
    MongoSampleService mongoSampleService;
    
    public static void main(String[] args) {
        SpringApplication.run(SampleDbAccessApp.class, args);
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        PostgresSample postgre = new PostgresSample();
        postgre.setName("John");
        postgresSampleService.save(postgre);
        List<PostgresSample> findResult1 = postgresSampleService.findAll();
        findResult1.forEach(System.out::println);
        
        MongoSample mongo = new MongoSample();
        mongo.setName("Kate");
        mongoSampleService.save(mongo);
        List<MongoSample> findResult2 = mongoSampleService.findAll();
        findResult2.forEach(System.out::println);
    }
}

1点言うことがあるとすれば、マルチモジュールプロジェクトで、コンポーネントのScanする場所が変わっています。
この場合、multi-module-commonプロジェクト配下なので、@ComponentScanで指定してあげる必要があります。 変数を使っていますが、これはAppConfig.javaにより定義して設定値から読み取っています。

使う側から見ると何の変哲もない感じで自然に扱えます。

参考

Spring bootでマルチデータソース対応の実装方法(MybatisとSpring Data JPA ) - Qiita
5.1. データベースアクセス(共通編) — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.0.1.RELEASE documentation
Multiple MongoDB connectors with Spring Boot