[Java][Spring Boot] PostgreSQLからレコードを取得してGZIP形式でS3にアップロードする。

2016.08.02

はじめに

AWS Redshiftのテーブルにレコードを挿入するには、S3のバケットに配置したCSVやTSVからCOPYを実行しますが、大容量データとなるとS3の使用料もかさむので、GZIP形式に圧縮して配置しています。
そこで今回は、Java・SpringBootでデータベースのレコードをGZIPでS3にアップロードする方法を調べた結果を備忘録として記しておきます。

目的

SpringBootを使用して、ローカルのPostgreSQLからレコードを取得し、GZIP形式でAWS S3にアップロードする。

環境

MAC OSX 10.10.5 Yosemite
Eclipse Mars
Java 8
Spring Boot 1.3.6.RELEASE
PostgreSQL 9.5.1

準備

テーブルを用意

CREATE TABLE fruit (
    id VARCHAR(2) NOT NULL,
    name VARCHAR(10),
    price integer,
    PRIMARY KEY(id)
);
 
INSERT INTO fruit VALUES
  ('1','apple',300),
  ('2','orange',200),
  ('3','banana',100),
  ('4','cherry',100),
  ('5','pineapple',500),
  ('6','melon',800),
  ('7','watermelon',600),
  ('8','strawberry',450)
  ;

コード

依存関係

dependencies {
	compile('org.springframework.cloud:spring-cloud-starter-aws')
	compile('org.springframework.boot:spring-boot-starter-data-jpa')
	compile('org.springframework.boot:spring-boot-starter-jdbc')
	compile('org.projectlombok:lombok:1.16.6')
	runtime('org.postgresql:postgresql')
	testCompile('org.springframework.boot:spring-boot-starter-test')
}

application.yml

cloud:
  aws:
    credentials:
      accessKey: aws_access_key
      secretKey: aws_secret_access_key
    region:
      static: リージョン名
spring: 
  datasource:
    url : jdbc:postgresql://localhost/データベース名
    username : ユーザー名
    password : パスワード
    driverClassName : org.postgresql.Driver

エンティティ

package com.locals3.blog;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.Getter;
import lombok.Setter;

@Entity
@Setter
@Getter
@Table(name="fruit")
public class Fruit {

	@Id
	private String id;
	private String name;
	private Integer price;

}

PostgreSQLのレコードと同じ構成。
10行目、Entityとして設定。
11~12行目、lombokでフィールドのid、name、priceにgetter/setterを設定。
13行目、対象テーブルを「fruit」に設定。

DAOインターフェース

package com.locals3.blog;

import java.io.Serializable;
import java.util.List;

public interface DaoInterface <T> extends Serializable {
	List<Fruit> getAllRecord();
}

7行目、DAOで使用するメソッドを1つだけ作成。

DAO

package com.locals3.blog;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPOutputStream;

import javax.persistence.EntityManager;
import javax.persistence.Query;

import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.WritableResource;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
public class DaoFruit implements DaoInterface<Fruit> {

	private EntityManager entityManager;
	private ResourceLoader resourceLoader;

	@Override
	public List<Fruit> getAllRecord() {
		Query query = entityManager.createQuery("from Fruit");
		List<Fruit> list = query.getResultList();
		return list;
	}

	public void s3upload(List<Fruit> list, String S3_BUCKET, int prefixNum) {

		Resource resource = this.resourceLoader.getResource(S3_BUCKET); 
		WritableResource writableResource = (WritableResource) resource;

		try (GZIPOutputStream out = new GZIPOutputStream(writableResource.getOutputStream());) {

			StringBuilder builer = new StringBuilder();
			Fruit fruit;			

			for (int i = 0; i < list.size(); i++) {
				fruit = list.get(i);
				builer.append(fruit.getId()).append("\t");
				builer.append(fruit.getName()).append("\t");
				builer.append(fruit.getPrice()).append("\n");
				out.write(builer.toString().getBytes());
				builer.delete(0, builer.length());
			}

			out.flush();

		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

}

20行目、lombokの@AllArgsConstructorで、フィールドのEntityManagerとResourceLoaderを引数にコンストラクタを生成。
21行目、lombokの@NoArgsConstructorで、デフォルトコンストラクタの生成。

27~32行目、PostgreSQLの対象テーブルの全レコードを取得して、Listで返します。

34~61行目、上記で取得したListをGZIP形式にしてS3にアップロードするメソッド。
36行目、S3にアップロードするバケットとファイル名を指定。

39~60行目、GZIP形式で書き込み。
45行目、Listに含まれる全レコードから1レコードを取得。
46~48行目、StringBuilderでTSV形式の文字列を作り上げ、
49行目、GZIPのストリームに書き込み。
50行目、次のループに備えStringBuilderを空にします。
53行目、S3に保存。

このDAOクラスでこのメソッドも含んでしまうのはあまり良い作りでは無いかもしれませんが、分かり易いかなと思いこの形にしました。

コントローラー

package com.locals3.blog;

import java.util.List;

import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;

@Component
public class TestController {

	private DaoFruit dao;
	private final String S3_BUCKET = "s3://バケット名/保存したいファイル名";
	
	@PersistenceContext
	private EntityManager entityManager;

	@Autowired // ここで@Autowiredしないと何故かnullになる...
	private ResourceLoader resourceLoader;

	@PostConstruct
	public void init() {
		dao = new DaoFruit(entityManager, resourceLoader);
	}
	
	public void getAllAndUpload() {
		List<Fruit> list = dao.getAllRecord();
		dao.s3upload(list, S3_BUCKET);
	}

}

13行目、クラスを@Component化。@Configurationでも良いみたいです。

22~23行目、このクラスで使用しないResourceLoaderを@Autowiredしています。
次に出てくるDAOクラスで使用するのですが、ここで@AutowiredしないとNullExceptionが呼ばれてしまいます。
最初、ここに気付かずDAOクラスで@Autowiredしていて少しハマりました...

25~28行目、メソッドを@PostConstructでアノテートしているので、コンストラクタ生成後のクラスの使用前に呼ばれます。
初期化処理が必要な場合に使用すると良さそうです。
27行目、DAOにEntityManagerとResourceLoaderを渡して初期化しています。
これでDAOの準備ができました。

30~33行目、このメソッドでテーブルの全レコードを取得してS3にアップロードする流れをまとめています。

起動クラス

package com.locals3.blog;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class LocalToS3Application {

	public static void main(String[] args) {
		try (ConfigurableApplicationContext cac = SpringApplication.run(LocalToS3Application.class, args);) {
			TestController app = cac.getBean(TestController.class);
			app.getAllAndUpload();
		}
	}
	
}

今回は@RestControllerを使用せず、アプリケーションが起動したら、そのまま最後まで進みます。

11~13行目、コントローラーのBeanを取得してメソッドgetAll()を呼び出しています。
11行目、ConfigurableApplicationContextは、Closeableを継承したクラスなのでtry-with-resourcesで囲みます。

実行結果

AWSコンソールからS3の対象バケットを見ると、設定したファイル名でアップロードされていると思います。

さいごに

目的を実現するためにググった結果、GZIPでローカルに保存をしたり、S3にアップロードしたりといった事は個別に解説されたページは発見できましたが、一連の流れで解説されたページを発見できなかったので書きました。
特に、取得データをローカルに保存せず、S3に直接GZIPでアップロードする流れは無かったかなと思います。(日本語では...)