Spring Boot, Spring Data JPA, MySQL

Trong bài viết này tôi sẽ hướng dẫn các bạn xây dựng project thao tác CRUD (Create Read Update Delete) với cơ sở dữ liệu MySQL thông qua Spring Boot và Spring Data JPA. Các bạn có thể làm quen với Spring Boot qua bài viết Spring Boot – Hello World

Đối với các ví dụ có liên quan đến MySQL tôi thường sử dụng XAMPP, vì cài đặt nhanh và quản lý dễ nhưng mục tiêu là chỉ để phục vụ cho các ví dụ hoặc các demo. Trên CentOS, Ubuntu hoặc RedHat thì có cách thức cài đặt/quản trị khác. Download XAMPP theo link bên dưới:

https://www.apachefriends.org/download.html

Sau khi download và cài đặt theo link bên trên thì các bạn mở XAMPP Control Panel và chạy MySQL server trên XAMPP

Sau khi MySQL đã start-up thành công. Trên XAMPP Control Panel, các bạn click lên button Shell. Giờ truy cập đến MySQL và chạy đoạn script bên dưới để tạo table “fstack_book”

mysql -uroot -p

fstack_book.sql

CREATE SCHEMA IF NOT EXISTS `fstack_blog` DEFAULT CHARACTER SET utf8 ;
USE `fstack_blog` ;
CREATE TABLE IF NOT EXISTS `fstack_book` (
  `id` INT NOT NULL,
  `title` VARCHAR(45) NOT NULL,
  `author` VARCHAR(45) NOT NULL,
  `description` TEXT NULL,
  `created` DATE NULL,
  `updated` DATE NULL,
  `status` TINYINT NULL,
  PRIMARY KEY (`id`))
ENGINE = InnoDB;

Bây giờ chúng ta sẽ viết Simple REST API để thao tác với MySQL dựa trên Spring Boot và Spring Data JPA.

Khởi tạo project như hướng dẫn ở bài Spring Boot – Hello World!

Bổ sung thêm: BookController.java, BookRepository.java và BookEntity.java như cấu trúc bên dưới:

BookEntity.java – entity này sẽ mappinng với bảng ‘fstack_book’

package vn.fstack.springboot.entity;

import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;

@Entity
@Table(name = "fstack_book")
public class BookEntity {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;

	@Column(name = "title", nullable = false)
	private String title;

	@Column(name = "author", nullable = false)
	private String author;

	@Column(name = "description", nullable = true)
	private String description;

	@Column(name = "created", nullable = false, updatable = false)
	@Temporal(TemporalType.TIMESTAMP)
	@CreatedDate
	private Date created;

	@Column(name = "updated", nullable = false)
	@Temporal(TemporalType.TIMESTAMP)
	@LastModifiedDate
	private Date updated;

	@Column(name = "status", nullable = false)
	private Boolean status;

	/* gets and sets methods */

}

BookRepository.java

package vn.fstack.springboot.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import vn.fstack.springboot.entity.BookEntity;

@Repository
public interface BookRepository extends JpaRepository<BookEntity, Long> {	

}

Vì sao lại dùng JpaRepository?

Trong Spring framework thì Spring Data Repositories có thể xử lý hầu hết các cơ sở dữ liệu SQL và Non-SQL. JpaRepository là một phần của Spring Data Repositories, nó kế thừa từ PagingAndSortingRepository và PagingAndSortingRepository kế thừa từ CrudRepository.

@NoRepositoryBean
public interface PagingAndSortingRepository extends CrudRepository {
...
}
  • CrudRepository – cung cấp các phương thức Create, Read, Update, Delete.
  • PagingAndSortingRepository – cung cấp các phương thức phân trang (paging) và sắp sếp dữ liệu của các records trước khi trả về cho client.
  • JpaRepository – cung cấp cac phương thức liên quan đến flushing cho persistence context và detelet dữ liệu theo lô (batch).

Vì vậy đó là lý do ta sử dụng JpaRepository, nhưng cũng tùy hệ thống mà các bạn có thể sử dụng CrudRepository hay PagingAndSortingRepository.

BookController.java – cung cấp các endpoint cho phép thao tác với fstack_book

package vn.fstack.springboot.controller;

import java.util.Date;
import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import vn.fstack.springboot.entity.BookEntity;
import vn.fstack.springboot.repository.BookRepository;

@RestController
@RequestMapping(value = { "book" })
public class BookController {

	@Autowired
	private BookRepository bookRepository;
	
	@GetMapping
	public ResponseEntity<List<BookEntity>> getAllBook() {
		List<BookEntity> books = bookRepository.findAll();
		return new ResponseEntity<List<BookEntity>>(books, HttpStatus.OK);
	}

	@GetMapping(value = "/{id}")
	public ResponseEntity<BookEntity> getBookById(@PathVariable("id") long id) {
		Optional<BookEntity> book = bookRepository.findById(id);
        if (!book.isPresent()) {
            return new ResponseEntity("Not found book with id=" + id, HttpStatus.NOT_FOUND);
        }
		
		return new ResponseEntity<>(book.get(), HttpStatus.OK);
	}

	@PostMapping
	public ResponseEntity<BookEntity> postCreateNewBook(@RequestBody BookEntity book) {
		book.setCreated(new Date());
		book.setUpdated(new Date());
		BookEntity bookCreated = bookRepository.save(book);
		return new ResponseEntity<BookEntity>(bookCreated, HttpStatus.OK);
	}

	@PutMapping(value = "/{id}")
	public ResponseEntity<BookEntity> putCreateNewBook(@PathVariable("id") long id, @RequestBody BookEntity bookUpdate) {
		
		Optional<BookEntity> book = bookRepository.findById(id);
        if (!book.isPresent()) {
            return new ResponseEntity("Not found book with id=" + id, HttpStatus.NOT_FOUND);
        }
		
		BookEntity found = book.get();
		found.setTitle(bookUpdate.getTitle());
		found.setAuthor(bookUpdate.getAuthor());
		found.setDescription(bookUpdate.getDescription());
		found.setStatus(bookUpdate.getStatus());
		bookRepository.saveAndFlush(found);
		return new ResponseEntity<BookEntity>(found, HttpStatus.OK);
	}

	@DeleteMapping(value = "/{id}")
	public ResponseEntity<BookEntity> deleteBookById(@PathVariable("id") long id) {
		
		Optional<BookEntity> book = bookRepository.findById(id);
        if (!book.isPresent()) {
            return new ResponseEntity("Not found book with id=" + id, HttpStatus.NOT_FOUND);
        }
		
		BookEntity bookDeleted = book.get();
		bookRepository.deleteById(id);
		return new ResponseEntity<BookEntity>(bookDeleted, HttpStatus.OK);
	}

}

application.properties

server.port=7001

spring.datasource.url=jdbc:mysql://localhost:3306/fstack_blog?useUnicode=true&amp;characterEncoding=UTF-8 
spring.datasource.username=root
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.use-new-id-generator-mappings=false

spring.jpa.hibernate.ddl-auto

Dùng hibernate để khởi tạo scheme cho database, tham số này có các giá trị sau:

* validate: xác nhận lại schema và đánh dấu không có thay đổi tới database.
* update: cập nhật schema
* create: tạo schema và xóa các dữ liệu trước đó.
* create-drop: drop schema và tạo mới.
* none: không có tác động đến schema hiện tại

Build project với maven:

mvn clean install


[INFO] Scanning for projects...
[INFO] 
[INFO] ----------< vn.fstack:fstack-springboot-springdata-jpa-mysql >----------
[INFO] Building fstack-springboot-springdata-jpa-mysql 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-resources-plugin:3.1.0:resources (default-resources) @ fstack-springboot-springdata-jpa-mysql ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory C:\Users\PC\Documents\fstack-springboot-springdata-jpa-mysql\src\main\resources
[INFO] skip non existing resourceDirectory C:\Users\PC\Documents\fstack-springboot-springdata-jpa-mysql\src\main\resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ fstack-springboot-springdata-jpa-mysql ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 4 source files to C:\Users\PC\Documents\fstack-springboot-springdata-jpa-mysql\target\classes
[INFO] 
[INFO] --- maven-resources-plugin:3.1.0:testResources (default-testResources) @ fstack-springboot-springdata-jpa-mysql ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory C:\Users\PC\Documents\fstack-springboot-springdata-jpa-mysql\src\test\resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ fstack-springboot-springdata-jpa-mysql ---
[INFO] No sources to compile
[INFO] 
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ fstack-springboot-springdata-jpa-mysql ---
[INFO] 
[INFO] --- maven-jar-plugin:3.1.2:jar (default-jar) @ fstack-springboot-springdata-jpa-mysql ---
[INFO] Building jar: C:\Users\PC\Documents\fstack-springboot-springdata-jpa-mysql\target\fstack-springboot-springdata-jpa-mysql-0.0.1-SNAPSHOT.jar
[INFO] 
[INFO] --- spring-boot-maven-plugin:2.1.6.RELEASE:repackage (repackage) @ fstack-springboot-springdata-jpa-mysql ---
[INFO] Replacing main artifact with repackaged archive
[INFO] 
[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ fstack-springboot-springdata-jpa-mysql ---
[INFO] Installing C:\Users\PC\Documents\fstack-springboot-springdata-jpa-mysql\target\fstack-springboot-springdata-jpa-mysql-0.0.1-SNAPSHOT.jar to C:\Users\PC\.m2\globiots\repository\vn\fstack\fstack-springboot-springdata-jpa-mysql\0.0.1-SNAPSHOT\fstack-springboot-springdata-jpa-mysql-0.0.1-SNAPSHOT.jar
[INFO] Installing C:\Users\PC\Documents\fstack-springboot-springdata-jpa-mysql\pom.xml to C:\Users\PC\.m2\globiots\repository\vn\fstack\fstack-springboot-springdata-jpa-mysql\0.0.1-SNAPSHOT\fstack-springboot-springdata-jpa-mysql-0.0.1-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.683 s
[INFO] Finished at: 2019-09-16T08:29:13Z
[INFO] ------------------------------------------------------------------------

Chạy ứng dụng với maven:

mvn spring-boot:run

Hoặc bạn có thể start ứng dụng với lệnh java -jar:

java -jar fstack-springboot-springdata-jpa-mysql-0.0.1-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.6.RELEASE)

2019-09-19 11:01:26.468  INFO 1016 --- [           main] vn.fstack.springboot.Application         : Starting Application on DESKTOP-PO33MCF with PID 1016 (C:\Users\PC\Documents\fstack-springboot-springdata-jpa-mysql\target\classes started by PC in C:\Users\PC\Documents\fstack-springboot-springdata-jpa-mysql)
2019-09-19 11:01:26.473  INFO 1016 --- [           main] vn.fstack.springboot.Application         : No active profile set, falling back to default profiles: default
2019-09-19 11:01:27.348  INFO 1016 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.
2019-09-19 11:01:27.428  INFO 1016 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 71ms. Found 1 repository interfaces.
2019-09-19 11:01:27.877  INFO 1016 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$737c4cbb] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2019-09-19 11:01:28.409  INFO 1016 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 7001 (http)
2019-09-19 11:01:28.438  INFO 1016 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2019-09-19 11:01:28.438  INFO 1016 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.21]
2019-09-19 11:01:28.565  INFO 1016 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2019-09-19 11:01:28.565  INFO 1016 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2044 ms
2019-09-19 11:01:28.756  INFO 1016 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2019-09-19 11:01:28.985  INFO 1016 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2019-09-19 11:01:29.034  INFO 1016 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
	name: default
	...]
2019-09-19 11:01:29.127  INFO 1016 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {5.3.10.Final}
2019-09-19 11:01:29.128  INFO 1016 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2019-09-19 11:01:29.304  INFO 1016 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.0.4.Final}
2019-09-19 11:01:29.471  INFO 1016 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.MySQL5Dialect
2019-09-19 11:01:30.163  INFO 1016 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2019-09-19 11:01:30.713  INFO 1016 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2019-09-19 11:01:30.769  WARN 1016 --- [           main] aWebConfiguration$JpaWebMvcConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2019-09-19 11:01:31.058  INFO 1016 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 7001 (http) with context path ''
2019-09-19 11:01:31.061  INFO 1016 --- [           main] vn.fstack.springboot.Application         : Started Application in 4.942 seconds (JVM running for 5.309)

Sử dụng công cụ POSTMAN để kiểm tra lại các REST API đã tạo

POST – http://localhost:7001/book/

GET – http://localhost:7001/book/9

PUT – http://localhost:7001/book/9

DELETE – http://localhost:7001/book/9

Source đầy đủ của ví dụ trong bài này có thể download trên Github qua link bên dưới:

https://github.com/thitranthanh/fstack-springboot-springdata-jpa-mysql