Java
spring

Spring Data JDBC Preview

Spring Data JDBCというプロジェクトができていたので、試してみます。

SpringのRDBアクセスといえばJdbcTemplateSpring Data JPAなどがあります。
まだCRUDサポートレベルですが、将来的にSpring Data JDBCもDBアクセスの有力な選択肢となるかもしれません。

今回試したソースはこちらです。

!!!2018年1月現在、Spring Data JDBCはBUILD-SNAPSHOTです。将来的にこの記事中のコードが動かなくなる可能性が十分あります。!!!

環境

  • jdk 1.8
  • Spring Boot 2.0.0.M7
  • Maven 3

pom.xml

Spring Bootプロジェクトを作成したら、pom.xmlにdependencyを追加します。DBはH2 Databaseにします。

pom.xml
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jdbc</artifactId>
            <version>1.0.0.BUILD-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>

テーブル定義

schema.sqlにテーブル定義を書きます。

schema.sql
create table employee (
    employee_number bigint primary key auto_increment,
    firstname varchar NOT NULL,
    lastname varchar NOT NULL,
    age int NOT NULL,
    hired_at date
);

Repositoryの作成

テーブルに対応したエンティティクラスを作成します。IDとなるフィールドには@Idアノテーションをつけます。
(Getter/Setter書くのが面倒だったのでLombokを使っていますw)

Employee.java
@Data
public class Employee {

    @Id
    private Long employeeNumber;
    private String firstname;
    private String lastname;
    private Integer age;
    private LocalDate hiredAt;

}

エンティティを作成したら、CrudRepositoryを継承したRepositoryを作ります。

EmployeeRepository.java
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
}

Bean定義

Spring Data JDBCを動かすには

  • @EnableJdbcRepositories
  • DataAccessStrategyのBean定義

が必要になります。
またカラム名をスネークケースにしたい場合は、エンティティのフィールド名<=>カラム名の変換を行うNamingStrategyを定義します。

SpringDataJdbcDemoApplication.java
@SpringBootApplication
@EnableJdbcRepositories
public class SpringDataJdbcDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringDataJdbcDemoApplication.class, args);
    }

    @Autowired
    JdbcMappingContext context;
    @Autowired
    DataSource datasource;

    @Bean
    DataAccessStrategy dataAccessStrategy() {
        return new DefaultDataAccessStrategy(
                new SqlGeneratorSource(context),
                new NamedParameterJdbcTemplate(datasource),
                context);
    }

    @Bean
    NamingStrategy namingStrategy() {
        return new CustomNamingStrategy();
    }
}
CustomNamingStrategy.java
public class CustomNamingStrategy extends DefaultNamingStrategy {

    @Override
    public String getColumnName(JdbcPersistentProperty property) {
        String propertyName = property.getName();
        return camelToSnake(propertyName);
    }

    @Override
    public String getTableName(Class<?> type) {
        return super.getTableName(type);
    }

    public String camelToSnake(String original) {
        char[] chars = original.toCharArray();
        char[] buff = new char[chars.length + 10];

        int j = 0;
        for(int i = 0; i < chars.length; i++) {
            char c = chars[i];
            if(buff[buff.length - 1] != ' ') {
                buff = Arrays.copyOf(buff, buff.length + 10);
            }
            if (Character.isUpperCase(c)) {
                buff[j++] = '_';
                buff[j++] = Character.toLowerCase(c);
            } else {
                buff[j++] = c;
            }
        }
        return new String(buff).trim();
    }

}

動かしてみる

さっそく動かしてみます。

@ExtendWith(SpringExtension.class)
@SpringBootTest
public class SpringDataJdbcDemoApplicationTests {

    @Autowired
    EmployeeRepository repo;

    @Test
    public void contextLoads() {
    }

    @Test
    public void test1() {
        Employee employee = new Employee();
        employee.setFirstname("John");
        employee.setLastname("Do");
        employee.setAge(30);
        employee.setHiredAt(LocalDate.of(2012, 4, 1));

        // Insert
        employee = repo.save(employee);
        Long employeeNumber = employee.getEmployeeNumber();

        Optional<Employee> insertedOpt = repo.findById(employeeNumber);
        assertTrue(insertedOpt.isPresent());
        Employee inserted = insertedOpt.get();
        assertAll("insert",
                () -> assertEquals(inserted.getEmployeeNumber(), employeeNumber),
                () -> assertEquals(inserted.getFirstname(), "John"),
                () -> assertEquals(inserted.getLastname(), "Do"),
                () -> assertEquals(inserted.getAge(), Integer.valueOf(30)),
                () -> assertEquals(inserted.getHiredAt(), LocalDate.of(2012, 4, 1)));

        // Update
        employee.setAge(31);
        repo.save(employee);

        Optional<Employee> updatedOpt = repo.findById(employeeNumber);
        assertTrue(updatedOpt.isPresent());
        Employee updated = updatedOpt.get();
        assertEquals(updated.getAge(), Integer.valueOf(31));

        // Delete
        repo.delete(employee);

        Optional<Employee> deleted = repo.findById(employeeNumber);
        assertTrue(!deleted.isPresent());
    }
}

MyBatis連携

試してないですが、MyBatisと連携させることも可能らしいです。
Repositoryのメソッドが呼ばれると、対応した名前のMapperのメソッドが実行されるようです。