基于Kotlin的 Spring Boot JPA应用

基于Kotlin的 Spring Boot JPA应用

  • 1. 简介
  • 2. pom.xml
    • 2.1 依赖
    • 2.2. JPA编译插件
    • 2.3 完整的 pom.xml 文件内容
  • 3. 使用Kotlin数据类定义JPA实体
    • 3.1 实体类
    • 3.2. 存储库类
  • 4. 应用程序入口
  • 5. 单元测试
  • 6. 总结
  • 参考文章

1. 简介

本文主要讲解如何在Kotlin中使用JPA。为了便于讲解,本文使用了一个Spring Boot JPA应用,其特征如下 :

  • 基于Spring Data JPA
  • 没有使用Web
  • 所有类通过Kotlin定义
  • 使用JUnit 5
  • 使用内嵌数据库H2用于演示

2. pom.xml

2.1 依赖

首先,我们要在pom.xml中引入依赖,声明使用Spring BootJPA :

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-jpaartifactId>
        dependency>

然后,我们使用H2内嵌式数据库支持数据保存 :

        <dependency>
            <groupId>com.h2databasegroupId>
            <artifactId>h2artifactId>
        dependency>

接下来,是跟Kotlin相关的依赖 , 这里kotlin.versionpom.xml属性定义区域定义 :

        <dependency>
            <groupId>org.jetbrains.kotlingroupId>
            <artifactId>kotlin-reflectartifactId>
            <version>${kotlin.version}version>
        dependency>

        <dependency>
            <groupId>org.jetbrains.kotlingroupId>
            <artifactId>kotlin-stdlibartifactId>
            <version>${kotlin.version}version>
        dependency>

单元测试有关的依赖 :

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
            <exclusions>
                <exclusion>
                    <groupId>junitgroupId>
                    <artifactId>junitartifactId>
                exclusion>
            exclusions>
        dependency>

        <dependency>
            <groupId>org.junit.jupitergroupId>
            <artifactId>junit-jupiter-engineartifactId>
            <scope>testscope>
        dependency>

2.2. JPA编译插件

要使用JPA,实体类需要一个无参构造函数。

缺省情况下,Kotlin数据类是没有无参构造函数的,为了产生无参构造函数,我们需要如下JPA插件:


            <plugin>
                <artifactId>kotlin-maven-pluginartifactId>
                <groupId>org.jetbrains.kotlingroupId>
                <version>${kotlin.version}version>
                <configuration>
                    <compilerPlugins>
                        
                        <plugin>jpaplugin>
                    compilerPlugins>
                configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.jetbrains.kotlingroupId>
                        <artifactId>kotlin-maven-noargartifactId>
                        <version>${kotlin.version}version>
                    dependency>
                dependencies>
            plugin>

2.3 完整的 pom.xml 文件内容


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>andy.kotlin.jpagroupId>
    <artifactId>zeroartifactId>
    <version>1.0-SNAPSHOTversion>

    <properties>
        <kotlin.version>1.3.50kotlin.version>
    properties>

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.1.8.RELEASEversion>
    parent>

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-jpaartifactId>
        dependency>

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
            <exclusions>
                
                <exclusion>
                    <groupId>junitgroupId>
                    <artifactId>junitartifactId>
                exclusion>
            exclusions>
        dependency>

        
        <dependency>
            <groupId>org.junit.jupitergroupId>
            <artifactId>junit-jupiter-engineartifactId>
            <scope>testscope>
        dependency>

        <dependency>
            <groupId>com.h2databasegroupId>
            <artifactId>h2artifactId>
        dependency>

        <dependency>
            <groupId>org.jetbrains.kotlingroupId>
            <artifactId>kotlin-reflectartifactId>
            <version>${kotlin.version}version>
        dependency>

        <dependency>
            <groupId>org.jetbrains.kotlingroupId>
            <artifactId>kotlin-stdlibartifactId>
            <version>${kotlin.version}version>
        dependency>

    dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>kotlin-maven-pluginartifactId>
                <groupId>org.jetbrains.kotlingroupId>
                <version>${kotlin.version}version>
                <configuration>
                    <compilerPlugins>
                        
                        <plugin>jpaplugin>
                    compilerPlugins>
                configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.jetbrains.kotlingroupId>
                        <artifactId>kotlin-maven-noargartifactId>
                        <version>${kotlin.version}version>
                    dependency>
                dependencies>
            plugin>
        plugins>
    build>
project>

3. 使用Kotlin数据类定义JPA实体

以上步骤完成后,现在可以通过Kotlin数据类定义JPA实体。在本文的例子中,我们定义了两个JPA实体:员工Employee和电话号码PhoneNumber , 如下所示 :

3.1 实体类

员工实体Employee定义在文件Employee.kt中 :

package andy

import javax.persistence.*

@Entity
data class Employee(
        /**
         * 员工记录 id,自动生成
         */
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        val id: Int,

        /**
         * 员工姓名,不可为空
         */
        @Column(nullable = false)
        val name: String,

        /**
         * 员工电子邮箱,可以为空
         */
        @Column(nullable = true)
        val email: String? = null,

        /**
         * 员工电话号码,可以有多个,关联存储在另外一张表中
         */
        @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER)
        val phoneNumbers: List<PhoneNumber>? = null
)

电话号码PhoneNumber实体定义在PhoneNumber.kt中 :

package andy

import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id

/**
 * 电话号码
 */
@Entity
data class PhoneNumber(
        /**
         * 电话号码记录 id,自动生成
         */
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        val id: Int,

        /**
         * 电话号码字符串值,不能为空
         */
        @Column(nullable = false)
        val number: String
)

从实体类定义可以看到,JPA提供的注解,比如@Entity,@Column@Id等等在这里可以随意使用。

3.2. 存储库类

有了JPA实体,我们来定义相应的存储库类。

首先是员工Employee实体对应的存储库类EmployeeRepository,定义在文件EmployeeRepository.kt中:

package andy

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
import org.springframework.stereotype.Repository

@Repository
interface EmployeeRepository : JpaRepository<Employee, Long>, JpaSpecificationExecutor<Employee> {
   fun findByNameLike(pattern: String): List<Employee>;
}

然后是电话号码PhoneNumber实体对应的PhoneNumberRepository存储库类,定义在文件PhoneNumberRepository.kt中:

package andy

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
import org.springframework.stereotype.Repository

@Repository
interface PhoneNumberRepository : JpaRepository<PhoneNumber, Long>, JpaSpecificationExecutor<PhoneNumber> {
}

4. 应用程序入口

有了上面的实体类和相应的存储库,我们定义一个Spring Boot应用在文件Application.kt中,如下所示 :

package andy

import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean

@SpringBootApplication
open class Application {
    // 另外一种注入 EmployeeRepository bean 的方法
    //@Autowired
    //lateinit var repository: EmployeeRepository;

    /**
     * 定义一个 CommandLineRunner bean,
     * 它会在应用启动时运行
     */
    @Bean
    open fun init(repository: EmployeeRepository): CommandLineRunner {
        return CommandLineRunner {
            // 这段逻辑对应接口 CommandLineRunner 约定的方法  void run(String... args)
             
            // 注意下面定义的 Employee 实例的第三个参数设置为了 null,
            // 这里可以设置为 null 是因为 Employee 实体定义中相应 phoneNumber 属性定义中的问号
            repository.save(Employee(0, "张三", "[email protected]", null))
            repository.save(Employee(0, "李四", "[email protected]", null))
            repository.save(Employee(0, "王五", "[email protected]", null))

            /**
             * 上面的添加了一个Employee 王五,所以下面输出语句肯定能搜索到一条记录
             */
            val entity = repository.findByNameLike("王五")[0];
            println("查询到的记录应该是 王五 : $entity");
        }
    }
}


fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

在该文件中:

  1. 我们使用注解@SpringBootApplication定义了一个Spring Boot应用类Application
  2. 又定义了一个Spring Boot CommandLineRunner bean,接口CommandLineRunner约定了该beanrun方法会在容器启动时被执行;
  3. 定义方法main启动应用Application;

该文件中重点的部分是CommandLineRunner bean的定义。它有如下特征 :

  1. 以一个Kotlin函数的形式存在,接受一个参数repository: EmployeeRepository以接收注入的EmployeeRepository bean;
  2. 返回值是一个CommandLineRunner实例,表明其run方法会在应用启动时被调用;

另外,我们在CommandLineRunner#run方法逻辑中往数据库插入三条员工记录,但是每个员工的电话号码都未设置,使用了null值。这里主要是用来演示数据库插入动作和null值的使用的。运行该程序,你可以看到如下输出 :

查询到的记录应该是 王五 : Employee(id=3, name=王五, email=wang.wu@test.com, phoneNumbers=[])

5. 单元测试

这里我们制作一个基于JUnit 5的单元测试来观察一下Kotlin中使用JPA :

package andy

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.boot.test.context.SpringBootTest
import java.util.*


@DataJpaTest
@DisplayName("Test JPA in Kotlin using JUnit 5")
open class KotlinJPATest @Autowired constructor(val employeeRepository: EmployeeRepository) {

    // 另外一种注入方式,属性注入
    @Autowired
    lateinit var phoneNumberRepository: PhoneNumberRepository;

    @Test
    fun testJPAInKotlin() {
        /**
         * 以下语句添加三个员工,使用六个电话号码
         */

        val tom = Employee(0, "Tom", "[email protected]", Arrays.asList(PhoneNumber(0, "110"), PhoneNumber(0, "120")));
        employeeRepository.save(tom);

        val jerry = Employee(0, "Jerry", "[email protected]", Arrays.asList(PhoneNumber(0, "119"), PhoneNumber(0, "911")));
        employeeRepository.save(jerry);

        val andy = Employee(0, "Andy", "[email protected]", Arrays.asList(PhoneNumber(0, "114"), PhoneNumber(0, "10080")));
        employeeRepository.save(andy);


        /**
         * 下面查询语句能匹配到一条记录,是关于 Jerry 的
         */
        val employees: List<Employee> = employeeRepository.findByNameLike("%rr%");
        println("查询得到的员工记录是 : $employees ");
        assertEquals(1,employees.size,"查询得到的员工记录数量应该是1")
        assertEquals("Jerry",employees[0].name,"查询得到的员工记录应该是Jerry")

        /**
         * 下面输出电话号码的数量,应该是 6,也就是上面创建员工记录时所使用的电话号码信息
         */
        val countPhoneNumbers = phoneNumberRepository.count();
        println("电话号码记录数量 : $countPhoneNumbers");
        assertEquals(6,countPhoneNumbers,"电话号码数量 [应该是 6]")
    }
}

该单元测试我们主要演示以下几个要点 :

  1. 通过构造函数注入组件;
  2. 使用@Autowired注入组件;
  3. Kotlin中像在Java中一样操作JPA实体和存储库组件;

运行该测试,在控制台上,你应该可以看到如下输出 :

Hibernate: insert into employee (id, email, name) values (null, ?, ?)
Hibernate: insert into phone_number (id, number) values (null, ?)
Hibernate: insert into phone_number (id, number) values (null, ?)
Hibernate: insert into employee (id, email, name) values (null, ?, ?)
Hibernate: insert into phone_number (id, number) values (null, ?)
Hibernate: insert into phone_number (id, number) values (null, ?)
Hibernate: insert into employee (id, email, name) values (null, ?, ?)
Hibernate: insert into phone_number (id, number) values (null, ?)
Hibernate: insert into phone_number (id, number) values (null, ?)
Hibernate: select employee0_.id as id1_0_, employee0_.email as email2_0_, employee0_.name as name3_0_ from employee employee0_ where employee0_.name like ? escape ?
查询得到的员工记录是 : [Employee(id=5, name=Jerry, email=jerry@test.com, phoneNumbers=[PhoneNumber(id=3, number=119), PhoneNumber(id=4, number=911)])] 
Hibernate: select count(*) as col_0_0_ from phone_number phonenumbe0_
电话号码记录数量 : 6

6. 总结

本文通过一个例子应用讲解了如何使用Kotlin制作一个Spring Boot JPA应用,演示了如下要点 :

  1. 依赖的引入
  2. 插件的引入
  3. JPA实体的定义
  4. JPA存储库组件的定义
  5. SpringApplication入口应用程序的定义
  6. 基于JUnit 5的单元测试
  7. 基于构造函数的依赖注入
  8. 基于属性的依赖注入@Autowired
  9. JPA实体对象的创建
  10. 通过JPA存储库插入或者查询实体对象

参考文章

  • Building web applications with Spring Boot and Kotlin
  • Working with Kotlin and JPA

你可能感兴趣的:(Spring,Boot,Kotlin)