vlambda博客
学习文章列表

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

Working with Spring Data JPA and MySQL

在本章中,我们将继续我们在 Spring 中处理数据的旅程。如您所知,我们定义的 API 不会持久化或从任何真实数据源读取。我们将通过演示 Spring 强大的数据特性来解决这个问题。我们将使用 Spring Data JPA 和 MySQL 作为我们的数据库。

在本章中,我们将介绍以下主题:

  • Introducing Spring Data JPA
  • Installing MySQL
  • CRUD operations
  • Creating database queries

Introducing Spring Data JPA

Spring Data JPA 是 Spring Data 的一部分。 Spring Data 比 Spring Data JPA 大得多,所以我们将从解释它开始。 Spring Data作为基于Spring的编程,为我们提供了misc底层数据存储的机制。

感谢 Spring Data,我们可以利用不同的数据存储选项。我们可以访问不同的关系或非关系数据库。在本书中,我们将关注 MySQL 数据库。作为父级,Spring 包含多个子项目;其中之一是 Spring Data JPA。

What does Spring Data provide?

让我们重点介绍一下 Spring Data 提供的一些最重要的特性:

  • Repository and custom object-mapping abstractions
  • Derivation of dynamic queries from the repository method names
  • Auditing
  • Custom repositories
  • Ease of integration and configuration
  • Ease of use with Spring MVC controllers

Which Spring Data modules do we need?

对我们来说,最重要的模块如下:

  • Spring Data Commons, core components for Spring Data
  • Spring Data JPA, components for the implementation of JPA repositories

About Spring Data JPA

正如我们所说,Spring Data JPA 包含用于实现 JPA 存储库的组件。借助 Spring Data JPA,可以轻松创建使用数据访问技术的应用程序。

Spring Data JPA 提供了以下一组特性:

  • Support for creating repositories based on Spring and JPA
  • Support for Querydsl (http://www.querydsl.com/) predicates
  • Auditing
  • Pagination support
  • Support for dynamic query execution
  • Support for custom data access implementation
  • @Query annotated queries validation performed at Bootstrap time
  • @EnableJpaRepositories annotation to configure JavaConfig-based repositories

Installing MySQL

由于我们将重点关注 MySQL 数据库作为我们的存储,因此如果您尚未安装它,则必须在系统上安装它。出于我们的目的,MySQL Community Server 将完成这项工作。去https://dev.mysql.com/downloads/mysql/ 下载安装对于您的操作系统:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

您的操作系统执行安装过程:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

Installing MySQL Community Server on macOS

对于 macOS,MySQL 有两种变体:

  • DMG with the installation wizard
  • TAR archive—we must extract its content before installation

我们将使用 DMG 选项作为首选选项。打开 DMG 文件并启动安装包:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

按照安装说明进行操作。安装完成后,您将获得 MySQL 实例的 root 凭据:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

点击OK确认。关闭安装程序并弹出 DMG 映像。

现在打开 系统偏好 | MySQL:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

您将看到您的本地实例状态。如果它没有运行,请启动它:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

我们选择不通过系统启动来启动 MySQL。那是你的决定。

Installing MySQL Community Server on Windows

在开始安装之前,请确保您已安装 Microsoft Visual C++ 2013 Redistributable Package。如果没有,请从 Microsoft 下载中心安装。

运行设置。为您的操作系统选择安装类型。我们建议您使用 Developer Default。按照为您提供向导的安装说明进行操作。 MySQL 将被安装并启动。

Installing MySQL Community Server on Linux

我们将专注于使用通用二进制文件进行安装。我们可以通过下载压缩的 TAR 或使用系统的软件包管理器安装它来获得它。

Using a software package manager

我们将介绍最常用的软件包管理器。让我们从 YUM (DNF) 开始。启动终端并以特权用户身份运行命令。

使用 YUM 安装 MySQL:

  • Fedora 26:
$ dnf install https://dev.mysql.com/get/mysql57-community-release-fc26-10.noarch.rpm
  • Fedora 25:
$ dnf install https://dev.mysql.com/get/mysql57-community-release-fc25-10.noarch.rpm
  • Fedora 24:
$ dnf install https://dev.mysql.com/get/mysql57-community-release-fc24-10.noarch.rpm
  • CentOS 7 and Red Hat Enterprise Linux 7:
$ yum localinstall https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm
  • CentOS 6 and Red Hat Enterprise Linux 6:
$ yum localinstall https://dev.mysql.com/get/mysql57-community-release-el6-11.noarch.rpm

使用 DNF 安装 MySQL:

  • Fedora 26, 25, 24:
$ dnf install mysql-community-server 
  • CentOS 7, 6, and Red Hat Enterprise Linux 7, 6:
$ yum install mysql-community-server  

安装完成后,启动 MySQL 服务器本地实例并启用自动启动:

  • Fedora 26, 25, 24, CentOS 7, and Red Hat Enterprise Linux 7:
$ systemctl start mysqld.service 
$ systemctl enable mysqld.service 
  • CentOS 6 and Red Hat Enterprise Linux 6:
$ service mysql start 
$ chkconfig --levels 235 mysqld on 

这一步非常重要!获取你的root密码:

$ grep 'A temporary password is generated for root@localhost' /var/log/mysqld.log | tail -1  

你应该得到这样的东西:

... A temporary password is generated for root@localhost: )O5La-SQAnha  

Using TAR

/usr/local/mysql 位置提取 TAR 内容。

然后按照以下程序进行:

$ groupadd mysql 
$ useradd -r -g mysql -s /bin/false mysql 
$ cd /usr/local 
$ tar zxvf [ path to your downloaded file  ].tar.gz 
$ ln -s [ full path to mysql] mysql 
$ cd mysql 
$ mkdir mysql-files 
$ chmod 750 mysql-files 
$ chown -R mysql . 
$ chgrp -R mysql . 
$ bin/mysql_install_db -user=mysql 
$ bin/mysqld --initialize --user=mysql 
$ bin/mysql_ssl_rsa_setup 
$ chown -R root . 
$ chown -R mysql data mysql-files 
$ bin/mysqld_safe --user=mysql & 
$ cp support-files/mysql.server /etc/init.d/mysql.server 

我们有一个 MySQL 本地服务器实例启动并运行。我们已经准备好进行一些 Spring Data JPA 开发了!

Creating a schema

我们需要一个用于我们的应用程序的模式。使用默认的 UTF-8 排序规则创建一个名为 journaler_api 的模式。为此,由您决定如何访问本地 MySQL 服务器实例并创建模式。我们将使用 MySQL Workbench,如以下屏幕截图所示:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

查看 SQL 脚本,如以下屏幕截图所示:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

应用 SQL 脚本,如以下屏幕截图所示:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

Extending dependencies

要使用 Spring Data JPA,请打开您的 build.gradle 并扩展它:

... 
dependencies { 
    ... 
    runtime('mysql:mysql-connector-java') 
    compile("org.springframework.boot:spring-boot-starter-data-jpa") 
    ... 
} 

我们提供了必要的依赖项,还添加了对 MySQL 连接器和 Spring Data JPA 的支持。接下来,我们需要做的是定义我们的数据源。打开 application.properties 文件,根据本地 MySQL 实例添加配置:

spring.datasource.url=jdbc:mysql://localhost/journaler_api?useSSL=false&useUnicode=true&characterEncoding=utf-8 
spring.datasource.username=root 
spring.datasource.password=YOUR_MYSQL_ROOT_PASSWORD 
spring.datasource.tomcat.test-on-borrow=true 
spring.datasource.tomcat.validation-interval=30000 
spring.datasource.tomcat.validation-query=SELECT 1 
spring.datasource.tomcat.remove-abandoned=true 
spring.datasource.tomcat.remove-abandoned-timeout=10000 
spring.datasource.tomcat.log-abandoned=true 
spring.datasource.tomcat.max-age=1800000 
spring.datasource.tomcat.log-validation-errors=true 
spring.datasource.tomcat.max-active=50 
spring.datasource.tomcat.max-idle=10 
spring.jpa.hibernate.ddl-auto=update 

对上述代码的解释如下:

  • The first line represents the path to the database:
spring.datasource.url= ... 
  • Then we set the username and password that we will use to access the database:
spring.datasource.username=root 
spring.datasource.password=YOUR_MYSQL_ROOT_PASSWORD 
  • Validate the connection before borrowing it from the pool:
spring.datasource.tomcat.test-on-borrow=true 
  • Connection validation interval:
spring.datasource.tomcat.validation-interval=30000
  • Query used to validate connections from the pool before returning them to the caller:
spring.datasource.tomcat.validation-query=SELECT 1
  • Flag to remove abandoned connections if they exceed the remove abandoned timeout:
spring.datasource.tomcat.remove-abandoned=true 
spring.datasource.tomcat.remove-abandoned-timeout=10000 
  • Log the stack trace of abandoned connections:
spring.datasource.tomcat.log-abandoned=true 
  • Time in milliseconds to keep this connection:
spring.datasource.tomcat.max-age=1800000 
  • Log validation errors:
spring.datasource.tomcat.log-validation-errors=true 
  • A maximum number of active connections that can be allocated from the pool at the same time:
spring.datasource.tomcat.max-active=50  
  • A maximum number of idle connections that should be kept in the pool:
spring.datasource.tomcat.max-idle=10
  • And finally, initialize the database using Hibernate:
spring.jpa.hibernate.ddl-auto=update 

构建并运行应用程序。如果您提供了良好的 URL 和正确的凭据,则不会有问题;应用程序将启动。如果出现任何问题,由于此配置,您将能够看到堆栈跟踪。

启动应用程序后,检查表是否出现在 journaler_api 数据库中:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

CRUD operations

为了能够对我们的数据执行 CRUD 操作,我们必须实现一些代码。我们将进行必要的更改并引入一些新的类。我们现在向您介绍的是每个 Spring 应用程序操作存储在关系数据库中的数据的标准。

使用 NoteRepository 接口创建一个名为 repository 的包,如下所示:

package com.journaler.api.repository 
 
import com.journaler.api.data.Note 
import org.springframework.data.repository.CrudRepository 
 
/** 
 * String is the type for ID we use. 
 */ 
interface NoteRepository : CrudRepository<Note, String> 
 
TodoRepository: 
package com.journaler.api.repository 
 
import com.journaler.api.data.Todo 
import org.springframework.data.repository.CrudRepository 
 
/** 
 * String is the type for ID we use. 
 */ 
interface TodoRepository : CrudRepository<Todo, String> 

两个接口都继承 CrudRepositoryCrudRepository 为我们带来以下功能:

  • save(): Saving one entity
  • saveAll(): Saving multiple entities
  • findById(): Returns entity with ID
  • existsById(): Does entity with ID exist
  • findAll(): Returns all entities
  • findAllById(): Returns all entities with ID
  • count(): Returns the number of entities available
  • deleteById(): Deletes entities with ID
  • delete(): Deletes one entity
  • deleteAll(): Deletes all entities received as argument
  • deleteAll() (without arguments): Deletes all entities

现在,当我们定义了存储库时,我们必须更新我们的实体:

  • Code for Note:
package com.journaler.api.data 
 
import org.hibernate.annotations.GenericGenerator 
import javax.persistence.* 
 
@Entity 
@Table(name = "note") 
data class Note( 
        @Id 
        @GeneratedValue(generator = "uuid2") 
        @GenericGenerator(name = "uuid2", strategy = "uuid2") 
        @Column(columnDefinition = "varchar(36)") 
        var id: String = "", 
        var title: String, 
        var message: String, 
        var location: String = "" 
) { 
 
    /** 
     * Hibernate tries creates a bean via reflection. 
     * It does the object creation by calling the no-arg constructor. 
     * Then it uses the setter methods to set the properties. 
     * 
     * If there is no default constructor, the following excpetion happens: 
     * org.hibernate.InstantiationException: No default constructor for entity... 
     */ 
    constructor() : this( 
            "", "", "", "" 
    ) 
 
} 

要将类用作数据库实体,请使用 @Entity 注释。要指定要使用的表名,请使用 @Table 注释,如我们的 Note 类示例中所示。 @Id 注释用于告诉 Spring 我们的 ID 将是什么字段。如您所见,我们使用 UUID2 作为数据的 ID。

  • Code for Todo:
package com.journaler.api.data 
 
import org.hibernate.annotations.GenericGenerator 
import javax.persistence.* 
 
@Entity 
@Table(name = "todo") 
data class Todo( 
        @Id 
        @GeneratedValue(generator = "uuid2") 
        @GenericGenerator(name = "uuid2", strategy = "uuid2") 
        @Column(columnDefinition = "varchar(36)") 
        var id: String = "", 
        var title: String, 
        var message: String, 
        var schedule: Long, 
        var location: String = "" 
) { 
 
    /** 
     * Hibernate tries creates a bean via reflection. 
     * It does the object creation by calling the no-arg constructor. 
     * Then it uses the setter methods to set the properties. 
     * 
     * If there is no default constructor, the following excpetion happens: 
     * org.hibernate.InstantiationException: No default constructor for entity... 
     */ 
    constructor() : this( 
            "", "", "", -1, "" 
    ) 
 
} 

我们现在定义 Todo 类的方式与定义 Note 类的方式相同。我们为这两个实体定义了一个存储库并更新了实体本身。我们将继续进行更改并在我们的服务中引入存储库。更新这两个服务以使用存储库,就像我们在以下示例中所做的那样:

  • Code for NoteService:
package com.journaler.api.service 
 
import com.journaler.api.data.Note 
import com.journaler.api.repository.NoteRepository 
import org.springframework.beans.factory.annotation.Autowired 
import org.springframework.stereotype.Service 
 
@Service("Note service") 
class NoteService { 
 
    @Autowired 
    lateinit var repository: NoteRepository 
 
    /** 
     * Returns all instances of the type. 
     * 
     * @return all entities 
     */ 
    fun getNotes(): Iterable<Note> = repository.findAll() 
 
    /** 
     * Saves a given entity. Use the returned instance for further operations as 
     * the save operation might have changed the entity instance completely. 
     * 
     * @param entity must not be {@literal null}. 
     * @return the saved entity will never be {@literal null}. 
     */ 
    fun insertNote(note: Note): Note = repository.save(note) 
 
    /** 
     * Deletes the entity with the given id. 
     * 
     * @param id must not be {@literal null}. 
     * @throws IllegalArgumentException in case the given {@code id} is {@literal null} 
     */ 
    fun deleteNote(id: String) = repository.deleteById(id) 
 
    /** 
     * Saves a given entity. Use the returned instance for further operations as 
     * the save operation might have changed the entity instance completely. 
     * 
     * @param entity must not be {@literal null}. 
     * @return the saved entity will never be {@literal null}. 
     */ 
    fun updateNote(note: Note): Note = repository.save(note) 
 
} 
  • Code for TodoService:
package com.journaler.api.service 
 
import com.journaler.api.data.Todo 
import com.journaler.api.repository.TodoRepository 
import org.springframework.beans.factory.annotation.Autowired 
import org.springframework.stereotype.Service 
 
 
@Service("Todo service") 
class TodoService { 
 
    @Autowired 
    lateinit var repository: TodoRepository 
 
    fun getTodos(): Iterable<Todo> = repository.findAll() 
 
    fun insertTodo(todo: Todo): Todo = repository.save(todo) 
 
    fun deleteTodo(id: String) = repository.deleteById(id) 
 
    fun updateTodo(todo: Todo): Todo = repository.save(todo) 
 
} 

Todo 服务的实现方式与 Note 服务完全相同,只是它没有任何文档化代码,因此更易于阅读。我们更新了使用存储库提供的功能的方法。这意味着这也会影响我们的控制器:

  • Code for NoteController:
@RestController 
@RequestMapping("/notes") 
class NoteController { 
    ... 
    @DeleteMapping( 
            value = "/{id}", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun deleteNote( 
            @PathVariable(name = "id") id: String 
    ) = service.deleteNote(id) 
    ... 
    @PostMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun updateNote( 
            @RequestBody note: Note 
    ) : Note = service.updateNote(note) 
    ... 
} 
  • Code for TodoController:
@RestController 
@RequestMapping("/todos") 
class TodoController { 
    ... 
    @GetMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun getTodos(): Iterable<Todo> = service.getTodos() 
    ... 
    @DeleteMapping( 
            value = "/{id}", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun deleteTodo( 
            @PathVariable(name = "id") id: String 
    ) = service.deleteTodo(id) 
    ... 
    @PostMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun updateTodo(@RequestBody todo: Todo): Todo = service.updateTodo(todo) 
    ... 
} 

让我们试试我们的应用程序。构建它并启动它。我们将检查每个 CRUD 操作。观察最新更改中引入的有效负载的变化。

Insert

我们将首先应用 Insert note

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

执行 API 调用并插入更多注释。消息的标题和内容将由您决定

对 API 调用的 TODO 版本执行相同操作:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

创建多个 TODO。

Update

写下一个 Note 和您刚刚创建的 TODO 之一的 ID。然后,尝试更新它们每个的 API 调用。更新注释将返回以下结果:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

更新时,TODO 将返回以下结果:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

Select

让我们看看我们的数据库中现在有什么。首先为 Notes 执行获取 API 调用:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

对 TODO 做同样的事情:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

如您所见,我们插入的所有项目都在那里,包括更新的项目。

Delete

选择要删除的 ID 并执行 API 调用以删除 Note 和 TODO。

先删除一个注释:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

然后删除 TODO:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

如果您执行获取 API 调用,您将看到已删除的项目丢失。

More regarding updates

让我们尝试再更新一次 API 调用。选择要更新的笔记。这一次,只为 Note 设置一个新标题,但忽略消息字段。如果你尝试更新,你会得到一个错误:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

我们出现了问题!让我们修复它!要执行成功的更新,您必须提供整个实体!所有未在有效负载中提供以更新 API 调用但使用默认值定义的字段将覆盖我们数据库中的原始值!使用从 API 调用中获得的整个有效负载。不要使用手动填充的实例触发更新!

Introducing DTOs

到目前为止,我们还没有创建任何 data 传输对象 (DTO)。是时候这样做了。我们将介绍另外两个领域。扩展 NoteTodo 类。添加创建和修改的字段:

  • Note:
package com.journaler.api.data 
 
import com.fasterxml.jackson.annotation.JsonInclude 
import org.hibernate.annotations.CreationTimestamp 
import org.hibernate.annotations.GenericGenerator 
import org.hibernate.annotations.UpdateTimestamp 
import java.util.* 
import javax.persistence.* 
 
@Entity 
@Table(name = "note") 
@JsonInclude(JsonInclude.Include.NON_NULL) 
data class Note( 
        ... 
        @CreationTimestamp 
        var created: Date = Date(), 
        @UpdateTimestamp 
        var modified: Date = Date() 
) { 
    ... 
} 
  • Todo:
package com.journaler.api.data 
 
import com.fasterxml.jackson.annotation.JsonInclude 
import org.hibernate.annotations.CreationTimestamp 
import 
 org.hibernate.annotations.GenericGenerator 
import org.hibernate.annotations.UpdateTimestamp 
import java.util.* 
import javax.persistence.* 
 
@Entity 
@Table(name = "todo") 
@JsonInclude(JsonInclude.Include.NON_NULL) 
data class Todo( 
        ... 
        @CreationTimestamp 
        var created: Date = Date(), 
        @UpdateTimestamp 
        var modified: Date = Date() 
) { 
    ... 
} 

使用这些时间戳,我们将跟踪创建/更新的时间戳。更新这两个字段将是全自动的。用户不需要在每次执行更新操作时都传递它们,但可以看到它。

我们还引入了以下注解:

@JsonInclude(JsonInclude.Include.NON_NULL) 

多亏了这一点,我们将在序列化过程中忽略空字段。使用以下实现创建 NoteDTO

package com.journaler.api.data 
 
import java.util.* 
 
data class NoteDTO( 
        var title: String, 
        var message: String, 
        var location: String = "" 
) { 
 
    var id: String = "" 
    var created: Date = Date() 
    var modified: Date = Date() 
 
    constructor(note: Note) : this( 
            note.title, 
            note.message, 
            note.location 
    ) { 
        id = note.id 
        created = note.created 
        modified = note.modified 
    } 
} 

TodoDTO 会非常相似,如下:

package com.journaler.api.data 
 
import java.util.* 
 
data class TodoDTO( 
        var title: String, 
        var message: String, 
        var schedule: Long, 
        var location: String = "" 
) { 
 
    var id: String = "" 
    var created: Date = Date() 
    var modified: Date = Date() 
 
    constructor(todo: Todo) : this( 
            todo.title, 
            todo.message, 
            todo.schedule, 
            todo.location 
    ) { 
        id = todo.id 
        created = todo.created 
        modified = todo.modified 
    } 
} 

我们有一个包含强制和辅助字段的主构造函数,它们会将 NoteTodo 实例转换为其 DTO 等效项。

我们必须更新我们的服务和控制器类。更新您的 NoteService 类以使用 DTO 类型:

@Service("Note service") 
class NoteService { 
 
    @Autowired 
    lateinit var repository: NoteRepository 
 
    fun getNotes(): Iterable<NoteDTO> = repository.findAll().map { it -> NoteDTO(it) } 
 
    fun insertNote(note: NoteDTO) = NoteDTO( 
            repository.save( 
                    Note( 
                            title = note.title, 
                            message = note.message, 
                            location = note.location 
                    ) 
            ) 
    ) 
 
    fun deleteNote(id: String) = repository.deleteById(id) 
 
    fun updateNote(noteDto: NoteDTO): NoteDTO { 
        var note = repository.findById(noteDto.id).get() 
        note.title = noteDto.title 
        note.message = noteDto.message 
        note.location = noteDto.location 
        note.modified = Date() 
        note = repository.save(note) 
        return NoteDTO(note) 
    } 
} 

TodoService 类执行相同的操作:

@Service("Todo service") 
class TodoService { 
 
    @Autowired 
    lateinit var repository: TodoRepository 
 
    fun getTodos(): Iterable<TodoDTO> = repository.findAll().map { it -> TodoDTO(it) } 
 
    fun insertTodo(todo: TodoDTO): TodoDTO = TodoDTO( 
            repository.save( 
                    Todo( 
                            title = todo.title, 
                            message = todo.message, 
                            location = todo.location, 
                            schedule = todo.schedule 
 
                    ) 
            ) 
    ) 
 
    fun deleteTodo(id: String) = repository.deleteById(id) 
 
    fun updateTodo(todoDto: TodoDTO): TodoDTO { 
        var todo = repository.findById(todoDto.id).get() 
        todo.title = todoDto.title 
        todo.message = todoDto.message 
        todo.location = todoDto.location 
        todo.schedule = todoDto.schedule 
        todo.modified = Date() 
        todo = repository.save(todo) 
        return TodoDTO(todo) 
    } 
} 

最后,更新您的控制器。首先更新 NoteController 类:

@RestController 
@RequestMapping("/notes") 
class NoteController { 
 
    @Autowired 
    private lateinit var service: NoteService 
 
    @GetMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun getNotes() = service.getNotes() 
 
    @PutMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun insertNote( 
            @RequestBody note: NoteDTO 
    ) = service.insertNote(note) 
 
    @DeleteMapping( 
            value = "/{id}", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun deleteNote( 
            @PathVariable(name = "id") id: String 
    ) = service.deleteNote(id) 
 
    @PostMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun updateNote( 
            @RequestBody note: NoteDTO 
    ): NoteDTO = service.updateNote(note) 
} 

更改 TodoController 类:

 @RestController 
@RequestMapping("/todos") 
class TodoController { 
 
    @Autowired 
    private lateinit var service: TodoService 
 
    @GetMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun getTodos(): Iterable<TodoDTO> = service.getTodos() 
 
    @PutMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun insertTodo( 
            @RequestBody todo: TodoDTO 
    ): TodoDTO = service.insertTodo(todo) 
 
    @DeleteMapping( 
            value = "/{id}", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun deleteTodo( 
            @PathVariable(name = "id") id: String 
    ) = service.deleteTodo(id) 
 
    @PostMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun updateTodo(@RequestBody todo: TodoDTO): TodoDTO = service.updateTodo(todo) 
 
} 

构建并运行您的应用程序。现在尝试使用 API 调用。您现在可以在不传递所有字段的情况下创建实体。注意创建/修改的时间戳。它们会自动更新。

Creating database queries

在结束本章之前,我们将向您介绍数据库查询。我们将定义几个 API 调用来触发我们定义的查询。编写查询很简单。有几种方法。我们将介绍最常见的方法。

在第一个示例中,我们将介绍 API 调用,它将查询并返回给我们所有安排在我们提供的日期之后的 TODO。打开 TodoRepository 并扩展它:

package com.journaler.api.repository 
 
import com.journaler.api.data.Todo 
import org.springframework.data.jpa.repository.Query 
import org.springframework.data.repository.CrudRepository 
 
/** 
 * String is the type for ID we use. 
 */ 
interface TodoRepository : CrudRepository<Todo, String> { 
 
    @Query("from Todo t where t.schedule > ?1") 
    fun findScheduledLaterThan(date: Long): Iterable<Todo> 
 
} 

我们使用 @Query 注释来定义我们的查询,并将一个参数传递给它。如您所见,此查询将返回所有安排在所提供日期之后的 TODO。现在打开 TodoService 并引入一个新方法:

fun getScheduledLaterThan(date: Date): Iterable<TodoDTO> { 
        return repository.findScheduledLaterThan(date.time).map { it -> TodoDTO(it) } 
} 

最后,让我们将它与 TodoController 类联系起来:

package com.journaler.api.controller 
 
import com.journaler.api.data.TodoDTO 
import com.journaler.api.service.TodoService 
import org.springframework.beans.factory.annotation.Autowired 
import org.springframework.http.MediaType 
import org.springframework.web.bind.annotation.* 
 
@RestController 
@RequestMapping("/todos") 
class TodoController { 
    ... 
    fun getTodosLaterThan( 
            @RequestBody payload: TodoLaterThanRequest 
    ): Iterable<TodoDTO> = service.getScheduledLaterThan(payload.date) 
} 

我们在这里引入了一个新类。我们引入了一个数据类,因此我们可以将日期传递给 API 调用。创建一个 TodoLaterThanRequest 类并将其定位在控制器包下。该类应该有一个简单的定义:

package com.journaler.api.controller 
import java.util.* 
data class TodoLaterThanRequest(val date: Date) 

构建并运行您的应用程序。确保您已插入多个 TODO,并为 schedule 字段设置了正确的日期。如果您尝试 API 调用,您将获得满足我们在查询中定义的条件的所有项目:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

Named queries

我们再举一个查询数据的例子。我们将向您介绍命名查询。我们将使用应用于实体类的 @NamedQuery 注释来定义命名查询。为了让您更好地理解我们在说什么,我们将扩展我们的代码。打开 Note 类并扩展它:

package com.journaler.api.data 
... 
@Entity 
@Table(name = "note") 
@JsonInclude(JsonInclude.Include.NON_NULL) 
@NamedQuery( 
        name = "Note.findByTitle", 
        query = "SELECT n FROM Note n WHERE n.title LIKE ?1" 
) 
data class Note( 
    ... 
) { 
    ... 
} 

我们定义了一个查询,它将查找所有带有我们通过 API 调用作为参数提供的标题的笔记。将触发查询的方法称为 findByTitle,因此它必须在我们的 NoteRepository 中定义:

... 
interface NoteRepository : CrudRepository<Note, String> { 
    fun findByTitle(title: String): Iterable<Note> 
} 

剩下的就是通过添加方法将它与 NoteService 连接起来:

fun findByTitle(title: String): Iterable<NoteDTO> { 
        return repository.findByTitle(title).map { it -> NoteDTO(it) } 
} 

并将服务连接到控制器类:

... 
@RestController 
@RequestMapping("/notes") 
class NoteController { 
    ... 
    @PostMapping( 
            value = "/by_title", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
 
    ) 
    fun getTodosLaterThan( 
            @RequestBody payload: NoteFindByTitleRequest 
    ): Iterable<NoteDTO> = service.findByTitle(payload.title) 
 
} 

如您所见,我们还为有效负载定义了 NoteFindByTitleRequest 类:

package com.journaler.api.controller 
 
data class NoteFindByTitleRequest(val title: String) 

构建并运行您的应用程序。如果您尝试新创建的 API 调用,您应该能够通过 title 搜索 Notes,就像我们所做的那样:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Spring Data JPA和MySQL

Summary

在本章中,我们向您介绍了 Spring Data JPA 和 MySQL。我们创建了与数据库建立通信所需的所有类。我们不仅使 CRUD 操作成为可能,而且还演示了如何为我们的应用程序编写自定义查询。在下一章中,我们将在 Spring 开发中向前迈出一步。我们将通过引入 Spring Security 来限制对 API 某些部分的访问。