前回は、「Amazon RDS」の概要について紹介した上で、RDS DBインスタンスを作成するところまでの手順を説明しました。

続く今回は、Spring Data JPA(Java Persistence API)およびSpring Cloud AWSの概要を説明した上で、RDSへのテーブル構築とアプリケーション実装を進めていきます。

Spring Data JPAおよびSpring Cloud AWSの概要

Spring Data JPAは、Java EE(Enterprize Edition)の標準データベースアクセス仕様であるJPAを使用して、Repositoryを中核のコンポーネントとするデータベースアクセスで処理を抽象化し、CRUD操作などの実装を提供するフレームワークです。

基本的なリレーショナルデータベースへのCRUDアクセスであれば、エンティティクラスとレポジトリインタフェースを作成するだけで、SQLを記述することなく処理を実装できます。

Spring Data JPAは、以下のような機能/実装を提供します。

  • Javaのジェネリクスを利用した、GenericDAOパターンによるCRUD処理の実装
  • 命名規約に基づくメソッド名からの自動SQLクエリの組み立て/実行
  • 楽観ロックのサポート

JPAやSpring Data JPAの詳しい説明は本連載では割愛しますが、詳細を知りたい場合はTERASOLUNAデータベースアクセス(JPA)のガイドラインを適宜参照してください。

一方、Spring Cloud AWSは、Spring Cloudプロジェクトの1つで、AWSのS3やSQS、SNSといったサービスのSDKやAPIを統合するフレームワークです。以下のようなサポートを提供し、AWSサービスとの統合を実現します。

  • Amazon SQSにおけるSpring Messaging APIの実装
  • AWS ElastiCacheにおけるSpring Cache APIの実装
  • Amazon SNSにおけるアノテーションをベースとしたエンドポイントの設定/マッピング
  • CloudFormationで定義した論理名リソースへのアクセス
  • AWS RDSインスタンスの論理名からのデータソースの自動構築
  • Amazon S3におけるResourceLoaderを使ったBucketアクセス

今回のメイントピックスであるRDSに関しては、後ほど詳述しますが、Spring Cloud AWSではアプリケーションからアクセスするデータソースの自動構築をサポートします。

また、実際に作成したアプリケーションはGitHub上にコミットしています。以降に記載するソースコードでは、import文など本質的でない記述を省略している部分があるので、実行コードを作成する際は、必要に応じて適宜GitHubにあるソースコードも参照してください。

RDSアクセスアプリケーションの実装に入る前に

Spring Data JPAとSpring Cloud AWSを使用するには、まず、Mavenプロジェクトのpom.xmlで、spring-boot-starter-data-jpaおよび、spring-cloud-starter-awsのライブラリを定義します。また、RDSへアクセスするためには、spring-cloud-starter-aws-jdbcも合わせて追加する必要があります。なお、PostgreSQLのドライバや、エンティティクラスの実装を簡易化するためのLombokライブラリも追加しておきましょう。

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-aws</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-aws-jdbc</artifactId>
  </dependency>
  <dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </dependency>
</dependencies>

アプリケーションの実装を始める前に、Spring Cloud AWSとSpring Data JPAを使って、アプリケーションがRDSアクセスする際の仕組みを押さえておく必要があります。

通常、RDSとJavaアプリケーションを接続する場合は、javax.sql.DataSourceの実装インスタンスにRDSエンドポイントとデータベースのユーザー名、パスワードを設定し、それからConnectionを取得すればアクセスできます。

前回も説明したように、RDSはセキュリティグループによりアクセス制御されるので、インバウンド接続許可設定されたIPアドレス帯であれば、アプリケーション内でDataSourceからConnectionを取得する実装で接続できます。

しかし、Spring Cloud AWSを使ったRDSへの接続では、AWSユーザー認証情報およびRDSインスタンスの論理名とデータベース接続ユーザーのパスワードから、RDSエンドポイントなどの必要な情報を取得し、DataSourceオブジェクトを構築します。そのため、Spring Data JPAと組み合わせる場合は、JavaConfig上でSpring Cloud AWSによって自動作成されたDataSourceオブジェクトをインジェクションするよう設定すればOKです。

なお、下図の点線で示すようにSpring Cloud AWSを使用せず、直接DataSourceを構築する方法でも問題はありません。

Spring Cloud AWSを使ったRDSへの接続の仕組み

また、前回構築したRDSデータベースにテーブルを作成します。今回は以下のようなテーブル構成とします。

テーブル名 物理名 説明
グループ GRP グループを表すエンティティ。所属という実体関連を通してユーザーとN:Nの関連を持つ。なおGROUPはPostgreSQLで予約語のため簡略化表記
所属 MEMBERSHIP 所属を表すエンティティ。グループとユーザーにあるN対Nの関連を、N:1、1:Nのかたちで繋ぐ実体関連テーブル
ユーザー USR ユーザーを表すエンティティ。なおUSERはPostgreSQLで予約語のため簡略化表記
住所 ADDRESS 住所を表すエンティティ。ユーザーとは1:1の関連を持つ
メール Email メールを表すエンティティ。ユーザーとは1:Nの関連を持つ

テーブル構成

各テーブルを構築するDDLについては、GitHubにSQLファイルを公開しているので、そちらを参照してください。

まず、セキュリティグループでRDSのインバウンド接続が許可されたIPアドレスを持つEC2インスタンスなどで、PSQLクライアントなどをインストールします。続いて、前回作成したRDSのエンドポイント、データベース、ユーザー名を指定してRDSに接続後、DDLを記載したSQLファイルをインポートするiコマンドを実行すれば、テーブルを構築できます。

[centos@ip-XXXX-XXX-XXX-XXX ~]$ psql -U username -d sample_database -h mynavi-sample-db.xxxxxxx.ap-northeast-1.rds.amazonaws.com
Password for user username:
psql (9.4.0, server 10.6)
sample_database=> \i sample_database.sql

なお、CentOSのPSQLインストール方法ですが、EC2でCentOS 7を実行した場合は、以下のコマンドでPSQLをインストールできます。

[centos@ip-XXXX-XXX-XXX-XXX ~]$ sudo yum update -y
// omit
[centos@ip-XXX-XXX-XXX-XXX ~]$ sudo yum install -y postgresql

設定クラスの実装

それでは、いよいよアプリケーションの実装に進みます。今回作成するアプリケーションの構成は以下の通りです。

コンポーネント 説明 必須/任意
App SpringBootアプリケーションを実行する起動クラス 必須
DomainConfig サービスレイヤの設定を行うクラス 任意
JpaConfig JPAに関する設定クラス 必須
RDSConfig RDSへの接続に関する設定クラス 必須
SampleService 各Repositoryを使ってデータ設定するサービスを実装。トランザクション境界を設定 任意
Xxxxx(Entity) データベースに定義したテーブルに対応するエンティティクラス 必須
XxxxxRepository 各エンティティに対応するレポジトリ 必須

以降、各々のクラスについて解説を進めていきます。事前にAWS公式ページ「設定ファイルと認証情報ファイル」を参考に、ユーザーのホームフォルダに.awsディレクトリを作成し、「credential」というファイルを作成して前回取得したCSV形式の認証キーのユーザー認証情報を以下の形式で保存しておいてください。

[default]
aws_access_key_id=XXXXXXXXXXXXXXXX
aws_secret_access_key=YYYYYYYYYYYYYYYYYYYYYYYYYYYYY

それでは、実装していくクラスについて説明しましょう。

まずは、SpringBoot起動クラスと、各種設定クラスです。@SpringBootApplicaitonアノテーションが付与された起動クラスは、同一パッケージにある「@Configurationアノテーションが付与された設定クラス」と「設定クラス内で@ComponentScanされたパッケージにあるクラス」を読み取ります。今回は、目的に応じて以下の3つの設定クラスを作成します。

  • DomainConfigクラス:Serviceをコンポーネントスキャンで読み取る設定クラス
  • RdsConfigクラス:RDSの接続を行う設定クラス
  • JpaConfigクラス:Spring Data JPAの接続を行う設定クラス

設定クラスは必ずしも複数である必要はなく、1つにまとめても動作上は問題ありません。ただし、クラス名と役割を対応付けて作成しておいたほうが、後から見たときに設定内容をクラス名から識別できてベターです。

起動クラスはアプリケーションを構築後、SampleServiceクラスを取得し、RDSの各テーブルにデータを設定するメソッドを呼び出します。



package org.debugroom.mynavi.sample.aws.rds.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.debugroom.mynavi.sample.aws.rds.domain.service.SampleService;

@SpringBootApplication
public class App {

    public static void main(String[] args) {

        ApplicationContext applicationContext = SpringApplication.run(App.class, args);
        SampleService sampleService = applicationContext.getBean(SampleService.class);

        sampleService.setData();
    }

}

続いてDomainConfigクラスでは、Serviceなどビジネスロジックレイヤに属するコンポーネントがあるパッケージをスキャンする定義を追加しておきます。



package org.debugroom.mynavi.sample.aws.rds.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("org.debugroom.mynavi.sample.aws.rds.domain.service")
public class DomainConfig {
}

次に、RDSへの接続情報を設定するRdsConfigですが、@EnableRdsInstanceアノテーションで、RDSの論理名とDBユーザーのパスワードを設定します。



package org.debugroom.mynavi.sample.aws.rds.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.cloud.aws.jdbc.config.annotation.EnableRdsInstance;

@Configuration
@EnableRdsInstance(
    dbInstanceIdentifier = "${rds.identifier}",
    password ="${rds.password}",
    readReplicaSupport = false)
public class RdsConfig {
}

セキュリティ上ハードコーディングは望ましくないので、プロパティファイル上でプレースホルダとして設定し、以下のようにapplicaiton.ymlを通じて環境変数から取得できるように定義しておきます。

rds:
  identifier: ${RDS_IDENTIFIER}
  password: ${RDS_PASSWORD}

これらの設定を行うことにより、Spring Cloud AWSがRDSから論理名の紐付くエンドポイントやPostgreSQLのユーザー情報を取得して、DataSourceオブジェクトを構築するようになります。

なおここで、applicaiton-dev.ymlでプレースホルダにデフォルト値を設定するかたちで、前回RDSを構築した際に指定した論理名とパスワードを実装しておきます。

rds:
  identifier:  ${RDS_IDENTIFIER:mynavi-sample-db}
  password: ${RDS_PASSWORD:xxxxxxxxxx}

JpaConfigでは、@EnableTransactionManagementを付与したConfigクラスを作成し、生成されたDataSourceオブジェクトをorg.springframework.orm.jpa.LocalContainerEntityManagerFactoryBeanに設定します。

また、エンティティクラスのパッケージや、トランザクションマネージャのBean定義、@EnableJpaRepositoriesでレポジトリクラスのパッケージも指定しておいてください。



package org.debugroom.mynavi.sample.aws.rds.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.util.Properties;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages="org.debugroom.mynavi.sample.aws.rds.domain.repository")
public class JpaConfig {

    @Autowired
    DataSource dataSource;

    @Bean
    public PlatformTransactionManager transactionManager() throws Exception{
        return new JpaTransactionManager();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(){

        JpaVendorAdapter adapter = new HibernateJpaVendorAdapter();

        Properties properties = new Properties();
        properties.setProperty("hibernate.show_sql", "true");
        properties.setProperty("hibernate.format_sql", "true");

        LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean();
        emfb.setPackagesToScan("org.debugroom.mynavi.sample.aws.rds.domain.model.entity");
        emfb.setJpaProperties(properties);
        emfb.setJpaVendorAdapter(adapter);
        emfb.setDataSource(dataSource);

        return emfb;
     }

}

また通常、Spring Cloud AWSは、AWSのEC2インスタンスで実行されることを想定して、インスタンスプロファイル情報を取得する設定になっています。インスタンスプロファイル情報とは、アプリケーションを実行するEC2インスタンス、もしくはECSコンテナが持つ認証情報です。インスタンスやコンテナを実行する際、IAMロールから権限を割り当てるオプションによって設定されます。

開発端末では当然、認証情報の取得エラーになるので、開発端末下だけで有効となるapplication-dev.ymlで、「cloud.aws.credentials.profile」に空白を設定し、「instaceProfile」の値を「false」にしておきましょう。また、リージョンの自動検出をOFFにするよう、「cloud.aws.region.auto」に「false」を、RDSが構築されているリージョンを「cloud.aws.region.static」に手動で設定しておきます(以下参照)。

※ 詳細については、AWSドキュメントの「Amazon EC2 の IAM ロール」を参照してください。

cloud:
  aws:
    credentials:
      profileName:
      instanceProfile: false
    region:
      auto: false
      static: ap-northeast-1

この設定により、Spring Cloud AWSは、com.amazonaws.auth.DefaultAWSCredentialsProviderChainを利用して、AWSの認証情報を取得していくようになります。DefaultAWSCredentialsProviderChainでは、以下の順序で情報を取得していく実装になっています。

  • 環境変数AWSACCESSKEYIDとAWSSECRETACCESSKEY
  • システムプロパティaws.accessKeyIdとaws.secretKey
  • ユーザーのAWS認証情報ファイル
  • AWSインスタンスプロファイルの認証情報

これまでの設定では、ローカル環境ではユーザーのAWS認証情報ファイルで取得する状態になっていますが、実はこの実装のまま本番環境へ持って行っても、最後のAWSインスタンスプロファイルから認証情報を取得できます。この辺りの挙動の詳細を知りたい方は、実装コード「DefaultAWSCredentialsProviderChain」を参照してみてください。

開発環境/本番環境が変わるからといって、環境依存をなくすためにアプリケーション内でアクセスキーIDやシークレットキーを割り当てる実装は必要ないので、開発環境(AWS内ではないローカル環境の場合)は.aws配下の認証情報を取得し、本番環境では、インスタンスのプロファイル上から認証情報を取得する実装を心掛けてください。

なお、アプリケーション起動時に、Spring Cloud AWSのAuto-configurationであるContextStackAutoConfigurationによって、アプリケーションのスタック名自動検出が有効になり、AWS CloudFormationのスタックが見つからない場合、AmazonServiceExceptionが発生してアプリケーションが起動しなくなります。そのため、以下のようにapplication.ymlで「cloud.aws.stack.auto」の値に「false」を設定し、スタックの自動検出を無効化することでエラーを回避します。

cloud:
  aws:
    stack:
      auto: false

一方、アプリケーション実行時に例外「java.lang.reflect.InvocationTargetException Caused by: java.sql.SQLFeatureNotSupportedException: org.postgresql.jdbc.PgConnection.createClob()」が発生するケースもあります。PostgreSQLにCLOB型が存在しないためにチェックメソッドがスローしている例外で、実行に支障はありませんが、出力を抑制したい場合は、クラスパス直下に「hibernate.properties」を作成し、「hibernate.jdbc.lob.noncontextualcreation」の値を「true」に設定すれば出力を抑制できます。

* * *

これで、設定クラスの実装まで完了しました。次回はアプリケーションのSpring Data JPAを使用した処理実装を進めていきます。

著者紹介


川畑 光平(KAWABATA Kohei)

某システムインテグレータにて、金融機関システム業務アプリケーション開発・システム基盤担当を経て、現在はソフトウェア開発自動化関連の研究開発・推進に従事。

Red Hat Certified Engineer、Pivotal Certified Spring Professional、AWS Certified Solutions Architect Professionalなどの資格を持ち、アプリケーション基盤・クラウドなどさまざまな開発プロジェクト支援にも携わる。

本連載の内容に対するご意見・ご質問は Facebook まで。