데이터베이스 접근에 대해서 설명한다.


Hello World

코드 작성

build.gradle

dependencies {
    compile 'org.hsqldb:hsqldb'
    compile 'org.springframework.boot:spring-boot-starter-jdbc'
}

src/main/java/sample/springboot/Main.java

package sample.springboot;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private JdbcTemplate jdbc;

    public void method() {
        this.jdbc.execute("CREATE TABLE TEST_TABLE (ID INTEGER NOT NULL IDENTITY, VALUE VARCHAR(256))");

        this.jdbc.update("INSERT INTO TEST_TABLE (VALUE) VALUES (?)", "hoge");
        this.jdbc.update("INSERT INTO TEST_TABLE (VALUE) VALUES (?)", "fuga");
        this.jdbc.update("INSERT INTO TEST_TABLE (VALUE) VALUES (?)", "piyo");

        List<Map<String, Object>> list = this.jdbc.queryForList("SELECT * FROM TEST_TABLE");
        list.forEach(System.out::println);
    }
}

실행 결과

{ID=0, VALUE=hoge}
{ID=1, VALUE=fuga}
{ID=2, VALUE=piyo}
  • 의존관계에 spring-boot-starter-jdbc과 사용하는 DB (org.hsqldb:hsqldb)를 추가한다.
  • 그러면 지정한 DB를 메모리 상에 올려서 사용할 수 있게 된다.
  • 메모리에 저장되기에 JVM이 정지하면 데이터가 손실된다.
  • HSQLDB 외에 H2 및 Derby를 동일하게 내장해서 이용이 가능하다.

데이터를 파일에 저장하기

application.properties

spring.datasource.url=jdbc:hsqldb:file:./db/testdb;shutdown=true
  • 프로퍼티 파일에 spring.datasource.url을 정의하여 JDBC 연결 가능한 URL을 지정할 수있다.
  • HSQLDB의 경우 URL에서 데이터를 파일에 저장할지 여부를 지정하는 것이기에 위와 같이 설정하면 데이터를 파일에 저장이 된다.



MySQL 테이블

로컬 MySQL을 이용한다.

test_table

idvalue
1hoge
2fuga
3piyo

코드 작성

build.gradle

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-jdbc'
    compile 'mysql:mysql-connector-java:5.1.35'
}

application.properties

spring.datasource.url=jdbc:mysql://localhost/test
spring.datasource.username=test
spring.datasource.password=test
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

src/main/java/sample/springboot/Main.java

package sample.springboot;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private JdbcTemplate jdbc;

    public void method() {
        List<Map<String, Object>> list = this.jdbc.queryForList("SELECT * FROM TEST_TABLE");
        list.forEach(System.out::println);
    }
}

실행 결과

{id=1, value=hoge}
{id=2, value=fuga}
{id=3, value=piyo}
  • application.properties 연결 설정을 추가하여, 외부 DB에 접속할 수있다.



JPA 이용

기본

build.gradle

dependencies {
-   compile 'org.springframework.boot:spring-boot-starter-jdbc'
+   compile 'org.springframework.boot:spring-boot-starter-data-jpa'
    compile 'org.hsqldb:hsqldb'
}

application.properties

spring.datasource.url=jdbc:hsqldb:file:./db/testdb;shutdown=true
spring.jpa.hibernate.ddl-auto=update

src/main/java/sample/springboot/jpa/MyEntity.java

package sample.springboot.jpa;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class MyEntity {

    @Id @GeneratedValue
    private Long id;
    private String value;

    public MyEntity(String value) {
        this.value = value;
    }

    private MyEntity() {}

    @Override
    public String toString() {
        return "MyEntity [id=" + id + ", value=" + value + "]";
    }
}

src/main/java/sample/springboot/jpa/MyEntityRepository.java

package sample.springboot.jpa;

import org.springframework.data.jpa.repository.JpaRepository;

public interface MyEntityRepository extends JpaRepository<MyEntity, Long> {
}

Main.java

package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.MyEntity;
import sample.springboot.jpa.MyEntityRepository;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private MyEntityRepository repository;

    public void method() {
        this.repository.save(new MyEntity("test"));

        this.repository.findAll().forEach(System.out::println);
    }
}

실행 결과

$ gradle bootRun
MyEntity [id=1, value=test]

$ gradle bootRun
MyEntity [id=1, value=test]
MyEntity [id=2, value=test]

$ gradle bootRun
MyEntity [id=1, value=test]
MyEntity [id=2, value=test]
MyEntity [id=3, value=test]
  • JPA를 사용하는 경우에는 org.springframework.boot:spring-boot-starter-data-jpa을 의존관계에 추가한다.
  • JPA 구현은 Hibernate가 이용된다.
  • 기본으로 테이블이 매번 다시 만들게 되므로 spring.jpa.hibernate.ddl-auto=update를 설정하고있다.
  • JpaRepository를 상속한 인터페이스를 정의하면 Spring이 데이터 접근을 해준다.

메소드 이름에서 쿼리 자동 생성

데이터베이스

hoge

idnumberstringvalue
11onehoge
21twofuga
31threepiyo
42fourhoge
52fivefoga
63sixpiyo
73sevenhoge

엔티티

src/main/java/sample/springboot/jpa/Hoge.java

package sample.springboot.jpa;

import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Hoge {

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private int number;
    private String string;
    @Embedded
    private Fuga fuga;

    @Override
    public String toString() {
        return "Hoge [id=" + id + ", number=" + number + ", string=" + string + ", fuga=" + fuga + "]";
    }
}

src/main/java/sample/springboot/jpa/Fuga.java

package sample.springboot.jpa;

import javax.persistence.Embeddable;

@Embeddable
public class Fuga {

    private String value;

    @Override
    public String toString() {
        return "Fuga [value=" + value + "]";
    }
}

저장소 인터페이스

src/main/java/sample/springboot/jpa/HogeRepository.java

package sample.springboot.jpa;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

public interface HogeRepository extends JpaRepository<Hoge, Long> {

    List<Hoge> findByNumber(int number);

    List<Hoge> findByNumberOrderByIdDesc(int number);

    List<Hoge> findByStringLike(String string);

    List<Hoge> findByNumberLessThan(int number);

    List<Hoge> findByStringIgnoreCase(String string);

    List<Hoge> findByFugaValue(String string);

    long countByStringLike(String string);

    List<Hoge> findByNumberAndStringLike(int number, String string);

    List<Hoge> findByNumberOrString(int number, String string);
}

기동확인

src/main/java/sample/springboot/jpa/Main.java

package sample.springboot;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.Hoge;
import sample.springboot.jpa.HogeRepository;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private HogeRepository repository;

    public void method() {
        print("findByNumber",              repository.findByNumber(1));
        print("findByNumberAndStringLike", repository.findByNumberAndStringLike(1, "%e"));
        print("findByNumberOrString",      repository.findByNumberOrString(2, "seven"));
        print("findByNumberOrderByIdDesc", repository.findByNumberOrderByIdDesc(2));
        print("findByStringLike",          repository.findByStringLike("t%"));
        print("findByNumberLessThan",      repository.findByNumberLessThan(3));
        print("findByStringIgnoreCase",    repository.findByStringIgnoreCase("FIVE"));
        print("findByFugaValue",           repository.findByFugaValue("hoge"));
        print("countByStringLike",         repository.countByStringLike("%o%"));
    }

    private void print(String methodName, List<Hoge> list) {
        System.out.println("<<" + methodName + ">>");
        list.forEach(System.out::println);
        System.out.println();
    }

    private void print(String methodName, long number) {
        System.out.println("<<" + methodName + ">>");
        System.out.println(number);
        System.out.println();
    }
}

실행 결과

<<findByNumber>>
Hoge [id=1, number=1, string=one, fuga=Fuga [value=hoge]]
Hoge [id=2, number=1, string=two, fuga=Fuga [value=fuga]]
Hoge [id=3, number=1, string=three, fuga=Fuga [value=piyo]]

<<findByNumberOrderByIdDesc>>
Hoge [id=5, number=2, string=five, fuga=Fuga [value=fuga]]
Hoge [id=4, number=2, string=four, fuga=Fuga [value=hoge]]

<<findByStringLike>>
Hoge [id=2, number=1, string=two, fuga=Fuga [value=fuga]]
Hoge [id=3, number=1, string=three, fuga=Fuga [value=piyo]]

<<findByNumberLessThan>>
Hoge [id=1, number=1, string=one, fuga=Fuga [value=hoge]]
Hoge [id=2, number=1, string=two, fuga=Fuga [value=fuga]]
Hoge [id=3, number=1, string=three, fuga=Fuga [value=piyo]]
Hoge [id=4, number=2, string=four, fuga=Fuga [value=hoge]]
Hoge [id=5, number=2, string=five, fuga=Fuga [value=fuga]]

<<findByStringIgnoreCase>>
Hoge [id=5, number=2, string=five, fuga=Fuga [value=fuga]]

<<findByFugaValue>>
Hoge [id=1, number=1, string=one, fuga=Fuga [value=hoge]]
Hoge [id=4, number=2, string=four, fuga=Fuga [value=hoge]]
Hoge [id=7, number=3, string=seven, fuga=Fuga [value=hoge]]

<<countByStringLike>>
3

<<findByNumberAndStringLike>>
Hoge [id=1, number=1, string=one, fuga=Fuga [value=hoge]]
Hoge [id=3, number=1, string=three, fuga=Fuga [value=piyo]]

<<findByNumberOrString>>
Hoge [id=4, number=2, string=four, fuga=Fuga [value=hoge]]
Hoge [id=5, number=2, string=five, fuga=Fuga [value=fuga]]
Hoge [id=7, number=3, string=seven, fuga=Fuga [value=hoge]]

hoge

idnumberstringvalue
11onehoge
21twofuga
31threepiyo
42fourhoge
52fivefoga
63sixpiyo
73sevenhoge
  • Repository를 상속한 인터페이스에 find~ 같은 메소드를 정의하면 Spring이 해석해서 쿼리를 자동으로 생성해 준다.
  • 기본은 findBy[조건으로하는 속성의 이름]으로 정의한다.
  • And와 Or로 연결이 가능하다.
  • OrderBy[속성 이름][Asc|Desc]으로 정렬을 할 수 있다.
  • Like를 붙이면 문자열 포함 검색 수 있다.
  • LessThan, GreaterThan, Between 등도 사용할 수 있다.
  • IgnoreCase를 붙이면 대소 문자 구분없이 비교할 수 있다.
  • count~ 하면 검색 결과의 엔티티 수를 얻을 수 있다.
  • 내장 가능 클래스의 속성을 조건으로하는 경우는 findBy[조합 가능한 클래스] [내장 가능 클래스의 속성]와 연결한다.

JPQL을 사용

src/main/java/sample/springboot/jpa/HogeRepository.java

package sample.springboot.jpa;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface HogeRepository extends JpaRepository<Hoge, Long> {

    @Query("SELECT h FROM Hoge h WHERE (h.id % 2) = 0")
    List<Hoge> findEvenIdEntities();
}

src/main/java/sample/springboot/Main.java

package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.HogeRepository;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private HogeRepository repository;

    public void method() {
        this.repository.findEvenIdEntities().forEach(System.out::println);
    }
}

실행 결과

Hoge [id=2, number=1, string=two, fuga=Fuga [value=fuga]]
Hoge [id=4, number=2, string=four, fuga=Fuga [value=hoge]]
Hoge [id=6, number=3, string=six, fuga=Fuga [value=piyo]]

hoge

idnumberstringvalue
11onehoge
21twofuga
31threepiyo
42fourhoge
52fivefoga
63sixpiyo
73sevenhoge

@Query 어노테이션을 메소드에 부여하여 JPQL을 지정할 수 있다. JPQL은 @Query의 value로 설정한다.

EntityManager을 이용

src/main/java/sample/springboot/Main.java

package sample.springboot;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.Hoge;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private EntityManager em;

    public void method() {
        TypedQuery<Hoge> query = this.em.createQuery("SELECT h FROM Hoge h WHERE h.id=:id", Hoge.class);
        query.setParameter("id", 3L);

        Hoge hoge = query.getSingleResult();

        System.out.println(hoge);
    }
}

실행 결과

Hoge [id=3, number=1, string=three, fuga=Fuga [value=piyo]]

@Autowired를 사용하여 일반적으로 EntityManager를 인젝션(주입)할 수 있다.





선언적 트랜잭션 사용

코드 작성

src/main/java/sample/springboot/jpa/MyService.java

package sample.springboot.jpa;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class MyService {

    @Autowired
    private HogeRepository repository;

    public void save(String value) {
        Hoge hoge = new Hoge(value);
        this.repository.save(hoge);
    }

    public void saveAndThrowRuntimeException(String value) {
        this.save(value);
        throw new RuntimeException("test");
    }

    @Transactional
    public void saveAndThrowRuntimeExceptionWithTransactional(String value) {
        this.saveAndThrowRuntimeException(value);
    }

    @Transactional
    public void saveAndThrowExceptionWithTransactional(String value) throws Exception {
        this.save(value);
        throw new Exception("test");
    }

    public void show() {
        this.repository.findAll().forEach(System.out::println);
    }
}

src/main/java/sample/springboot/jpa/Main.java

package sample.springboot;

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

import sample.springboot.jpa.MyService;

@SpringBootApplication
public class Main {

    public static void main(String[] args) throws Exception {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            MyService s = ctx.getBean(MyService.class);

            s.save("normal");

            try {
                s.saveAndThrowRuntimeException("runtime exception without @Transactional");
            } catch (Exception e) {}

            try {
                s.saveAndThrowRuntimeExceptionWithTransactional("runtime exception with @Transactional");
            } catch (Exception e) {}

            try {
                s.saveAndThrowExceptionWithTransactional("exception with @Transactional");
            } catch (Exception e) {}

            s.show();
        }
    }
}

실행 결과

Hoge [id=1, value=normal]
Hoge [id=2, value=runtime exception without @Transactional]
Hoge [id=4, value=exception with @Transactional]
  • @Transactional 어노테이션을 메소드에 부여하면, 그 메소드 앞뒤가 트랜잭션 경계가 된다.
  • 트랜잭션 경계 안에서 RuntimeException 및 그 서브 클래스가 throw 되면 트랜잭션은 롤백된다.
  • @Transactional 어노테이션이 부여되어 있지 않으면, Exception 및 그 서브 클래스가 throw 되면 경우 롤백되지 않는다.
  • Exception이 발생된 경우도 롤백되었으면 하는 경우는 @Transactional (rollbackFor = Exception.class)과 같이 설정한다.



Flyway으로 마이그레이션

코드 작성

build.gradle

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-data-jpa'
    compile 'org.hsqldb:hsqldb'
+   compile 'org.flywaydb:flyway-core'
}

application.properties

spring.jpa.hibernate.ddl-auto=none

src/main/resources/db/migration/V1__create_database.sql

CREATE TABLE HOGE (
    ID INTEGER NOT NULL IDENTITY,
    VALUE VARCHAR(256)
);

INSERT INTO HOGE (VALUE) VALUES ('HOGE');
INSERT INTO HOGE (VALUE) VALUES ('FUGA');
INSERT INTO HOGE (VALUE) VALUES ('PIYO');

Main.java

package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.HogeRepository;

@SpringBootApplication
public class Main {

    public static void main(String[] args) throws Exception {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private HogeRepository repository;

    public void method() {
        this.repository.findAll().forEach(System.out::println);
    }
}

실행 결과

Hoge [id=0, value=HOGE]
Hoge [id=1, value=FUGA]
Hoge [id=2, value=PIYO]
  • Flyway을 의존관계에 추가하면 서버 시작시에 마이그레이션을 실행하게 된다.
  • JPA를 사용하는 경우에는 JPA가 DB를 자동 생성하지 않도록하지 않으면 안되기 때문에, spring.jpa.hibernate.ddl-auto=none을 지정한다.



복수 데이터 소스 사용

기본

src/main/java/sample/springboot/PrimaryDataSourceConfiguration.java

package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

@Configuration
public class PrimaryDataSourceConfiguration {

    @Bean @Primary
    public DataSource createPrimaryDataSource() {
        return DataSourceBuilder
            .create()
            .driverClassName("org.hsqldb.jdbcDriver")
            .url("jdbc:hsqldb:mem:primary")
            .username("SA")
            .password("")
            .build();
    }

    @Bean @Primary
    public JdbcTemplate createPrimaryJdbcTemplate(DataSource ds) {
        return new JdbcTemplate(ds);
    }
}

src/main/java/sample/springboot/SecondaryDataSourceConfiguration.java

package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

@Configuration
public class SecondaryDataSourceConfiguration {

    @Bean @MySecondary
    public DataSource createSecondaryDataSource() {
        return DataSourceBuilder
                .create()
                .driverClassName("org.hsqldb.jdbcDriver")
                .url("jdbc:hsqldb:mem:secondary")
                .username("SA")
                .password("")
                .build();
    }

    @Bean @MySecondary
    public JdbcTemplate createSecondaryJdbcTemplate(@MySecondary DataSource ds) {
        return new JdbcTemplate(ds);
    }
}

src/main/java/sample/springboot/MySecondary.java

package sample.springboot;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.beans.factory.annotation.Qualifier;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
public @interface MySecondary {
}
MyDatabaseAccess.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

@Component
public class MyDatabaseAccess {

    private static final String CREATE_TABLE_SQL = "CREATE TABLE TEST_TABLE (VALUE VARCHAR(256))";
    private static final String INSERT_SQL = "INSERT INTO TEST_TABLE VALUES (?)";
    private static final String SELECT_SQL = "SELECT * FROM TEST_TABLE";

    @Autowired
    private JdbcTemplate primary;

    @Autowired @MySecondary
    private JdbcTemplate secondary;

    public void initialize() {
        this.primary.execute(CREATE_TABLE_SQL);
        this.secondary.execute(CREATE_TABLE_SQL);
    }

    public void insertPrimary(String value) {
        this.primary.update(INSERT_SQL, value);
    }

    public void insertSecondary(String value) {
        this.secondary.update(INSERT_SQL, value);
    }

    public void showRecords() {
        System.out.println("Primary >>>>");
        this.primary.queryForList(SELECT_SQL).forEach(System.out::println);

        System.out.println("Secondary >>>>");
        this.secondary.queryForList(SELECT_SQL).forEach(System.out::println);
    }
}

src/main/java/sample/springboot/Main.java

package sample.springboot;

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

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            MyDatabaseAccess db = ctx.getBean(MyDatabaseAccess.class);

            db.initialize();

            db.insertPrimary("primary!!");
            db.insertSecondary("secondary!!");

            db.showRecords();
        }
    }
}

동작 확인

콘솔 출력

Primary >>>>
{VALUE=primary!!}

Secondary >>>>
{VALUE=secondary!!}

설명

src/main/java/sample/springboot/PrimaryDataSourceConfiguration.java

    @Bean @Primary
    public DataSource createPrimaryDataSource() {
        return DataSourceBuilder
            .create()
            .driverClassName("org.hsqldb.jdbcDriver")
            .url("jdbc:hsqldb:mem:primary")
            .username("SA")
            .password("")
            .build();
    }

    @Bean @Primary
    public JdbcTemplate createPrimaryJdbcTemplate(DataSource ds) {
        return new JdbcTemplate(ds);
    }
  • @Bean을 사용하여 DataSource의 빈을 정의하고 있다 (createPrimaryDataSource()).
  • 만든 DataSource를 인수받게 되고, 또한 JdbcTemplate의 빈을 정의하고 있다 (createPrimaryJdbcTemplate()).
  • DataSource를 여러 정의 할 때 하나의 정의를 @Primary에서 주석한다.
    • @Primary은 기본적으로 주입되는 bean을 명시하는 어노테이션이다.
    • 빈의 후보가 복수 존재하는 상태에서 한정자를 지정하지 않으면 @Primary에서 어노테이션된 빈이 주입된다.
  • DataSource 인스턴스는 DataSourceBuilder를 사용하여 작성할 수 있다.

src/main/java/sample/springboot/SecondaryDataSourceConfiguration.java

    @Bean @MySecondary
    public DataSource createSecondaryDataSource() {
        return DataSourceBuilder
                .create()
                .driverClassName("org.hsqldb.jdbcDriver")
                .url("jdbc:hsqldb:mem:secondary")
                .username("SA")
                .password("")
                .build();
    }

    @Bean @MySecondary
    public JdbcTemplate createSecondaryJdbcTemplate(@MySecondary DataSource ds) {
        return new JdbcTemplate(ds);
    }
  • 두 번째 DataSource의 정의는 자작 한정자를 부여하고 있다.

src/main/java/sample/springboot/MyDatabaseAccess.java

    @Autowired
    private JdbcTemplate primary;

    @Autowired @MySecondary
    private JdbcTemplate secondary;
  • 삽입할 때 @Autowired 뿐이라면 @Primary에서 어노테이션한 쪽의 빈이 자작 한정자에서 어노테이션하면 해당 빈이 인젝션(주입)된다.
  • 나머지는 대체로 지금까지한대로 하면 데이터베이스 액세스가 가능하다.

선언적인 트랜잭션

여러 DataSource를 정의한 경우, 그대로라면 @Primary 아닌 데이터 소스에 대한 선언적인 트랜잭션을 사용할 수 없다.

@Primary이 아닌 데이터 소스로 선언적인 트랜잭션을 사용하는 경우는 다음과 같이 구현한다.

코드 작성

src/main/java/sample/springboot/PrimaryDataSourceConfiguration.java

package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
+ import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+ import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class PrimaryDataSourceConfiguration {

    @Bean @Primary
    public DataSource createPrimaryDataSource() {
        return DataSourceBuilder
            .create()
            .driverClassName("org.hsqldb.jdbcDriver")
            .url("jdbc:hsqldb:mem:primary")
            .username("SA")
            .password("")
            .build();
    }

    @Bean @Primary
    public JdbcTemplate createPrimaryJdbcTemplate(DataSource ds) {
        return new JdbcTemplate(ds);
    }

+   @Bean @Primary
+   public PlatformTransactionManager createTransactionManager(DataSource ds) {
+       return new DataSourceTransactionManager(ds);
+   }
}

src/main/java/sample/springboot/SecondaryDataSourceConfiguration.java

package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
+ import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+ import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class SecondaryDataSourceConfiguration {

+   public static final String TRANSACTION_MANAGER_NAME = "secondary-tx-manager";

    @Bean @MySecondary
    public DataSource createSecondaryDataSource() {
        return DataSourceBuilder
                .create()
                .driverClassName("org.hsqldb.jdbcDriver")
                .url("jdbc:hsqldb:mem:secondary")
                .username("SA")
                .password("")
                .build();
    }

    @Bean @MySecondary
    public JdbcTemplate createSecondaryJdbcTemplate(@MySecondary DataSource ds) {
        return new JdbcTemplate(ds);
    }

+   @Bean(name=SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
+   public PlatformTransactionManager createTransactionManager(@MySecondary DataSource ds) {
+       return new DataSourceTransactionManager(ds);
+   }
}

src/main/java/sample/springboot/MyDatabaseAccess.java

package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
+ import org.springframework.transaction.annotation.Transactional;

@Component
public class MyDatabaseAccess {

    private static final String CREATE_TABLE_SQL = "CREATE TABLE TEST_TABLE (VALUE VARCHAR(256))";
    private static final String INSERT_SQL = "INSERT INTO TEST_TABLE VALUES (?)";
    private static final String SELECT_SQL = "SELECT * FROM TEST_TABLE";

    @Autowired
    private JdbcTemplate primary;

    @Autowired @MySecondary
    private JdbcTemplate secondary;

    public void initialize() {
        this.primary.execute(CREATE_TABLE_SQL);
        this.secondary.execute(CREATE_TABLE_SQL);
    }

-   public void insertPrimary(String value) {
-       this.primary.update(INSERT_SQL, value);
-   }
-   
-   public void insertSecondary(String value) {
-       this.secondary.update(INSERT_SQL, value);
-   }

+   @Transactional
+   public void insertPrimary(String value, boolean rollback) {
+       this.primary.update(INSERT_SQL, value);
+       if (rollback) throw new RuntimeException("test exception");
+   }
+   
+   @Transactional(SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
+   public void insertSecondary(String value, boolean rollback) {
+       this.secondary.update(INSERT_SQL, value);
+       if (rollback) throw new RuntimeException("test exception");
+   }

    public void showRecords() {
        System.out.println("Primary >>>>");
        this.primary.queryForList(SELECT_SQL).forEach(System.out::println);

        System.out.println("Secondary >>>>");
        this.secondary.queryForList(SELECT_SQL).forEach(System.out::println);
    }

}

src/main/java/sample/springboot/Main.java

package sample.springboot;

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

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            MyDatabaseAccess db = ctx.getBean(MyDatabaseAccess.class);

            db.initialize();

            db.insertPrimary("primary commit!!", false);
            db.insertSecondary("secondary commit!!", false);

            try {
                db.insertPrimary("primary rollback!!", true);
            } catch (Exception e) {}

            try {
                db.insertSecondary("secondary rollback!!", true);
            } catch (Exception e) {}

            db.showRecords();
        }
    }
}

기동확인

콘솔 출력

Primary >>>>
{VALUE=primary commit!!}

Secondary >>>>
{VALUE=secondary commit!!}

설명

src/main/java/sample/springboot/PrimaryDataSourceConfiguration.java

    @Bean @Primary
    public PlatformTransactionManager createTransactionManager(DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }

src/main/java/sample/springboot/SecondaryDataSourceConfiguration.java

    public static final String TRANSACTION_MANAGER_NAME = "secondary-tx-manager";

    ...

    @Bean(name=SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
    public PlatformTransactionManager createTransactionManager(@MySecondary DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }
  • 여러 데이터 소스를 정의한 후에 선언적 트랜잭션을 사용하는 경우PlatformTransactionManager의 bean을 정의한다.
  • @Primary 쪽은 @Primary에서 어노테이션만으로 괜찮지만, 그렇지 않은 쪽은 bean 이름을 지정 둔다.

src/main/java/sample/springboot/MyDatabaseAccess.java

    @Transactional
    public void insertPrimary(String value, boolean rollback) {
        this.primary.update(INSERT_SQL, value);
        if (rollback) throw new RuntimeException("test exception");
    }
    @Transactional(SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
    public void insertSecondary(String value, boolean rollback) {
        this.secondary.update(INSERT_SQL, value);
        if (rollback) throw new RuntimeException("test exception");
    }
  • @Primary의 DataSource를 사용하는 경우에는 @Transactional 어노테이션이 부여함으로써 선언적 트랜잭션이 사용할 수있다.
  • @Primary가 아닌 DataSource를 사용하는 경우는 @Transactional의 value에 PlatformTransactionManager의 bean 이름을 지정해야 한다.

참고


'Spring Boot' 카테고리의 다른 글

[Spring Boot] 메일 송신  (0) 2017.10.29
[Spring Boot] 외부 설정 이용  (0) 2017.10.29
[Spring Boot] 데이터베이스 접근  (1) 2017.10.29
[Spring Boot] 배포  (0) 2017.10.29
[Spring Boot] Thymeleaf 엔진 사용  (0) 2017.10.29
[Spring Boot] WebJars 이용하기  (0) 2017.10.29
  1. GreatYun 2019.02.20 15:08 신고

    안녕하세요. 블로그 글 구경하다가.. 블로그 작성하실때 소스코드나 머리글 같은 표현하시는건 어떤방법을 사용하시는지 알수있을까요?

+ Recent posts