質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
PostgreSQL

PostgreSQLはオープンソースのオブジェクトリレーショナルデータベース管理システムです。 Oracle Databaseで使われるPL/SQLを参考に実装されたビルトイン言語で、Windows、 Mac、Linux、UNIX、MSなどいくつものプラットフォームに対応しています。

Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

Spring

Spring Framework は、Javaプラットフォーム向けのオープンソースアプリケーションフレームワークです。 Java Platform上に、 Web ベースのアプリケーションを設計するための拡張機能が数多く用意されています。

Q&A

1回答

3206閲覧

Spring+JPAを用い、1リクエスト内で複数DBを参照したいです。

kukukuSZ

総合スコア0

PostgreSQL

PostgreSQLはオープンソースのオブジェクトリレーショナルデータベース管理システムです。 Oracle Databaseで使われるPL/SQLを参考に実装されたビルトイン言語で、Windows、 Mac、Linux、UNIX、MSなどいくつものプラットフォームに対応しています。

Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

Spring

Spring Framework は、Javaプラットフォーム向けのオープンソースアプリケーションフレームワークです。 Java Platform上に、 Web ベースのアプリケーションを設計するための拡張機能が数多く用意されています。

0グッド

0クリップ

投稿2021/01/17 05:40

編集2021/01/20 04:51

Spring内でJPARepositoryを用いて、1リクエスト内に複数DBからデータを取得および書込を行いたいです。
具体的には、以下の通りです。
データ書込用DBに履歴の保存を行い、データ取得用DBからユーザの取得をそれぞれ行う想定です。
取得用DBは書込用DBのレプリケーションであり、秒単位で同期がされます。

該当のソースコード

// コントローラクラス @RestController public class TestController { @Autowired private HistoryService historyService; @Autowired private UserService userService; @RequestMapping("/test") public Object test(Object history) { historyService.register(history); return userService.get(); } } // 履歴サービスクラス @Service public class HistoryServiceImpl implements HistoryService { @Autowired private HistoryRepository historyRepository; @Transactional public void register(Object history) { historyRepository.save(history); } } // ユーザサービスクラス @Service public class UserServiceImpl implements UserService { @Autowired private UserRepository UserRepository; @Transactional(readonly=true) public Object get() { return UserRepository.findAll(); } }

試したこと


AbstractRoutingDataSourceを継承したクラスを作成、ThreadLocalを使用してcurrentLookupKeyを設定しました。
@Aspectと専用のアノテーションを作成して、処理前にDataSourceを決定できるようにしました。

参考:) https://monakaice88.hatenablog.com/entry/2018/07/28/172720

ユーザサービスクラス内の@Transactionalの下に専用アノテーションを付与して、取得用DBを参照するようにしましたが、
書込用DBから取得を行ってしまいます。

② ※ 2021/1/18追記
transactionmanagerを複製して、@Transactionalのvalueに明示的に指定しましたが、デフォルトのDBにアクセスしてしまいます。

// DB定義クラス @configuration public class DBconfig { // ①用 @Bean @Primary public RoutingDataSourceResolver multiDataSource() { RoutingDataSourceResolver resolver = new RoutingDataSourceResolver(); // スイッチするデータソースを設定 Map<Object, Object> dataSources = new HashMap<>(); dataSources.put(READ_ONLY_DATA_SOURCE_NAME, DB2()); dataSources.put(UPDATABLE_DATA_SOURCE_NAME, DB1()); resolver.setTargetDataSources(dataSources); resolver.setDefaultTargetDataSource(dataSources.get("UPDATABLE_DATA_SOURCE_NAME")); return resolver; } public DataSource DB1() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl(xxx1); dataSource.setUsername(xxx); dataSource.setPassword(xxx); dataSource.setDriverClassName(xxx); return dataSource; } public DataSource DB2() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl(xxx2); dataSource.setUsername(xxx); dataSource.setPassword(xxx); dataSource.setDriverClassName(xxx); return dataSource; } // ②用 デフォルトは"transactionManager"を指定 @Bean(name = "transactionManager") pubic PlatformTransactionManager create1() { return new DataSourceTransactionManager(DB1()); @Bean(name = "transactionManagerForReplica") pubic PlatformTransactionManager create2() { return new DataSourceTransactionManager(DB2()); } // サービスクラスでAutowired @Service public class UserServiceImpl implements UserService { @Autowired private UserRepository UserRepository; @Qualifier("transactionManagerForReplica") @Autowired private PlatformTransactionManager transactionManagerForReplica; @Transactional(readonly = true , value = "transactionManagerForReplica") public Object get() { return UserRepository.get(); } }

補足情報

初めて書き込むこともあり、拙い日本語ですが、知見のある方ご教示いただけませんでしょうか。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

作成されたAbstractRoutingDataSourceの拡張クラスや、アノテーションなどが何も示されていないので、上手くいくかわからないですが、参考にされたサイトの内容をそのままつくられたんだとすると、
自分でThreadLocalのDataSourceTypeを切り替えれば良いんじゃないでしょうか。

java

1 @RequestMapping("/test") 2 public Object test(Object history) { 3 4 DbContextHolder.setDataSourceType(Updatable); // ※自分で定義したデータソースを表すEnum 5 6 historyService.register(history); 7 8 DbContextHolder.setDataSourceType(ReadOnly); // 切り替え 9 10 return userService.get(); 11 } 12

アノテーションでのデータソース指定やアスペクトでの自動切換えなどは追加の便利機能というか、データソース切り替えの目的には関係ないところなので、

  • コンテナ内に2つのデータソースがBeanとして存在して、
  • それがRoutingDataSourceに登録されていて、
  • アプリはRoutingDataSourceを見ていて
  • RoutingDataSourceはThreadLocalのデータソースキーをLookupKeyしていて、
  • つながりに当たる各種名前に間違いがない

というところだけに集中してシンプルに作って、動いてから宣言的なやり方に作り替えていくようにしたらどうでしょうか。

※参考サイトのやり方自体は、Springが用意してくれている仕組みに乗っかって行われているわけなので、もし「このサイトの方法以外の解決はないか?」を質問されているのなら、無い、またはあってもスジが良いとは言えないのでは、と思います。

(追記)
サービスメソッドに@Transactionalがついているので、TransactionManagerに設定されているDataSourceが使用されているんじゃないでしょうか。

そこに作成されたRoutingDataSourceが刺さってない、ということかもしれません。参考サイトは@Transactionalついてないですし、そこ設定するコード含まれてないですね。

xml

1<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 2 <property name="dataSource" ref="dataSource"/> <--コレがRoutingDataSourceをrefしないとダメかも --> 3</bean>

Spring - データアクセス リファレンスドキュメント

(追記)
「spring.jpa.open-in-view」をfalseにしたら切り替わりました。以下、動いたソースです。

YAML

1spring: 2 datasource: 3 db1: 4 jdbc-url: jdbc:h2:./h2db/db1 5 username: dev 6 password: dev 7 driverClassName: org.h2.Driver 8 9 db2: 10 jdbc-url: jdbc:h2:./h2db/db2 11 username: dev 12 password: dev 13 driverClassName: org.h2.Driver 14 jpa: 15 open-in-view: false # <--コレ 16 hibernate: 17 ddl-auto: none

java

1@EnableWebMvc 2@Configuration 3public class WebConfig implements WebMvcConfigurer { 4 5 @Bean 6 @Primary 7 public RoutingDataSourceResolver multiDataSource() { 8 RoutingDataSourceResolver resolver = new RoutingDataSourceResolver(); 9 Map<Object, Object> dataSources = new HashMap<>(); 10 dataSources.put(DataSourceType.db1.name(), db1()); 11 dataSources.put(DataSourceType.db2.name(), db2()); 12 13 resolver.setTargetDataSources(dataSources); 14 resolver.setDefaultTargetDataSource(db1()); 15 return resolver; 16 } 17 18 @Bean 19 @ConfigurationProperties(prefix = "spring.datasource.db1") 20 public DataSource db1() { 21 return DataSourceBuilder.create().build(); 22 } 23 24 @Bean 25 @ConfigurationProperties(prefix = "spring.datasource.db2") 26 public DataSource db2() { 27 return DataSourceBuilder.create().build(); 28 } 29}

java

1public class DbContextHolder { 2 3 public enum DataSourceType { 4 db1, db2, 5 } 6 7 private static ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>(); 8 9 public static void setDataSourceType(DataSourceType type) { 10 contextHolder.set(type); 11 } 12 13 public static DataSourceType getDataSourceType() { 14 return contextHolder.get(); 15 } 16 17 public static void clear() { 18 contextHolder.remove(); 19 } 20}

java

1@Controller 2public class TestController { 3 4 @Autowired 5 Db1Service service1; 6 7 @Autowired 8 Db2Service service2; 9 10 @RequestMapping("/test") 11 public Object test() { 12 13 DbContextHolder.setDataSourceType(DataSourceType.db1); 14 List<Product> db1List = service1.get(); 15 16 DbContextHolder.setDataSourceType(DataSourceType.db2); 17 List<Product> db2List = service2.get(); 18 19 return "OK"; 20 } 21}

投稿2021/01/17 08:42

編集2021/01/18 23:09
umau

総合スコア805

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

kukukuSZ

2021/01/18 01:07

umauさま ご回答ありがとうございます。 拡張クラスと専用アノテーションについては参考サイトの内容をそのまま作成いたしました。 後程追記いたします。 > 自分でThreadLocalのDataSourceTypeを切り替えれば良いんじゃないでしょうか。 こちらもすでに試したのですが、うまくいきませんでした。 デバッグモードで確認したところ、実行順序は以下の通りとなっておりました。 ①determineCurrentLookupKey() → ②コントローラクラス到達 → ③各サービスを呼出 推測ですが、determineCurrentLookupKey()でDataSourceが決定されるのは、リクエスト開始(厳密にはスレッド開始)時点の1回のみなのかと思います。 なので、コントローラクラスの中でThreadLocalのデータソースキーを変更しても、既にDataSourceは決定されており、最初に入れたデータソースキーのみですべての処理を行ってしまいます。 ※@Repositoryなども@Autowired済です。 こちらは「試したこと」の範疇なので、もし他の実装方法があればと思ったのですが、確かに仰る通りです。申し訳ございません。
umau

2021/01/18 02:26

少し気付いたので、Transactionalアノテーションについて、追記しました。
kukukuSZ

2021/01/18 07:14 編集

ありがとうございます。 transactionManagerを複数作成して、@Trasactionalへ明示的に指定してもデフォルトの書込用DBを参照してしまいます。 お手数おかけして申し訳ないです。
umau

2021/01/18 22:59

こちらでも試してみて、確かにリクエスト内の初回だけ効くみたいな動きが再現されたんですが、 「spring.jpa.open-in-view」をfalseにしたら、指し先変わりました! 動いたソース追記しときます。
kukukuSZ

2021/01/19 07:00

ありがとうございます! 同じように実装したところ、こちらでも切り替えられました。 一通り動作確認取れましたら、ベストアンサー(?)にいたします。 ちなみにopen-in-view=falseというのは、entityManagerをトランザクション毎に作成するようになるというイメージなんですかね?
umau

2021/01/19 08:32

ネット見てるとTransactionInterceptorがEntityManagerをnewしてるような図はでてますね。 正確に中まで追ったわけではないので、どの単位で作り直してるのかはわからないですけど。 http://terasolunaorg.github.io/guideline/5.4.1.RELEASE/ja/ArchitectureInDetail/DataAccessDetail/DataAccessJpa.html#openentitymanagerinviewinterceptor JSPとかのViewでまでEntityをもっていって遅延ロードでViewからDBアクセスするような作りにされてる場合はダメかもですね。 https://tosi-tech.net/2018/08/open-session-in-view-pattern/
kukukuSZ

2021/01/20 06:25 編集

何度も申し訳ございません。 >JSPとかのViewでまでEntityをもっていって遅延ロードでViewからDBアクセスするような作りにされてる場合はダメかもですね。 というのは、htmlなど画面側で表示しようとしてもできないということでしょうか。 コントローラクラスのレスポンスを受けて画面表示させようと思いましたが、うまくできませんでした。 open-in-view=falseの状態で、切り替えを行わない場合はうまく画面表示できるのですが、切り替えした場合は画面表示できません。
umau

2021/01/20 04:36

JpaRepositoryのメソッドによっては取得されたEntityは実体ではなくProxyである場合があって、実際にEntityのフィールドが参照されるまでSQL発行を遅延させるようになってるモノがあるからだと思います。原因が正しいなら、以下のどれかが対策になるかもしれないですが、 1. Entityに「@Proxy(lazy=false)」を付ける 2. @Service内でEntityから別のオブジェクトに項目を移し替える 3. getOneをfindById等に変える(※getOneだけかどうかはわからないです) 遅延ロードさせるのは初期表示のパフォーマンスの為だと思うので、そこが問題になるようなら設計からの見直しがいるのかもしれません。
kukukuSZ

2021/01/21 01:50 編集

ご回答ありがとうございます。 提示いただいた対策については、1,2ともに表示不可でした。 対策2については、詳細は以下となっておりました。 ① コントローラでリクエスト受信&サービス呼出 ② サービス内でDBからデータ取得 ③ サービス内で表示用オブジェクトに入れ替え ④ サービスからコントローラへ表示用オブジェクトを返す (この時点では取得データは正常) ⑤ サービス終了&コントローラでレスポンスを受け取る (この時点でなぜかnullに) サービスのreturn時点ではデータは正常なのですが、コントローラで受けるとnullになってしまいます。 @RepositoryではfindAllで全取得しています。 遅延ロードという知見がなかったため、調べてみます。
kukukuSZ

2021/01/23 09:15 編集

こちらについて折衷案として、histとuserのエンドポイントをひとつずつ作成して、view側でそれぞれ1回ずつで呼び出しを行う形で検討しました。 しかし遅延読み込みの影響か、やはり2個目のリクエストであるuser取得処理がnullで返ってしまいます。 既存プロジェクトの改修ですので、設計から見直しというのは大変そうです…。 RoutingDataSource以外の実装を探しています。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問