Lập trình Spring với ứng dụng MyContact

MyContact là một ứng dụng mà mình thường viết mỗi khi học một ngôn ngữ hay công nghệ mới. MyContact chỉ là một ứng dụng CRUD đơn giản, cho phép người dùng quản lý danh bạ cá nhân. Theo mình viết ứng dụng thực tế là một trong những cách học hiệu qủa nhất. Thông qua tutorial này, mình hi vọng sẽ giúp các bạn hiểu rõ hơn về Spring cũng như cách áp dụng trong thực tế.

Spring là một đại gia đình với rất nhiều dự án bên trong. Trong tutorial này, mình sẽ chỉ sử dụng các dự án cơ bản sau:

  • Spring Boot để khởi tạo và chạy dự án
  • Spring MVC để xây dựng web app
  • Spring Data để thao tác với cơ sở dữ liệu, cụ thể mình sẽ dùng Spring Data JPA - một thành viên trong gia đình Spring Data

Các công nghệ khác mình sẽ sử dụng:

  • Hệ quản trị CSDL: MySQL
  • ORM framework: Hibernate (cái này đã được tích hợp sẵn trong Spring Data)
  • Template engine: Thymeleaf 2.1 (do phiên bản Spring Boot hiện tại mình đang dùng là 1.4.3 chưa hỗ trợ Thymeleaf 3.0)

Các công cụ bao gồm:

  • Ubuntu 16.04
  • JDK 1.8
  • Eclipse Neon đã cài đặt Spring Tool Suite

Source code của dự án: https://github.com/yuen26/spring-mycontact


Tổng quan về ứng dụng MyContact


Các chức năng

Ứng dụng bao gồm các chức năng:

  • Hiển thị danh sách liên hệ
  • Thêm liên hệ mới
  • Sửa liên hệ
  • Xóa liên hệ
  • Tìm kiếm liên hệ

Các giao diện

Trang danh sách liên hệ:
alt text

Trang thêm/sửa liên hệ:
alt text

Source code giao diện: https://github.com/ntaback26/mycontact-html

Cơ sở dữ liệu

Cơ sở dữ liệu chỉ có duy nhất 1 bảng Contact chứa tên, email và số điện thoại của liên hệ:

CREATE TABLE `contact` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `name` varchar(50) NOT NULL,
 `email` varchar(50) NULL,
 `phone` varchar(20) NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

Xây dựng kiến trúc dự án

Ứng dụng MyContact được xây dựng theo mô hình các lớp (layered). Mỗi một lớp sẽ bao gồm các thành phần có cùng chức năng. Với mô hình này, chúng ta có thể đảm bảo Nguyên lý đơn nhiệm (Single Responsibility), tránh chồng chéo giữa các thành phần, đồng thời inject các dependency cũng dễ dàng hơn. Chúng ta hoàn toàn có thể áp dụng mô hình này với Symfony Framework của PHP hay thậm chí cả AngularJS ...

alt text

Trong đó:

  • Domain là các POJO, ánh xạ từ các bảng CSDL
  • Repository là các interface trực tiếp truy cập và thao tác với CSDL
  • Service là các lớp có nhiệm vụ xử lý business logic, không trực tiếp truy cập vào CSDL mà sẽ lấy dữ liệu từ Repository, rồi chuyển cho Controller
  • Controller là các lớp chỉ quan tâm đến request của người dùng: đọc input, xử lý input, lấy dữ liệu từ Service đổ ra View. Mỗi một Controller sẽ có nhiều phương thức tương ứng với các use case riêng
  • View là các file html - giao diện người dùng

Chắc các bạn sẽ thắc mắc tại sao mình không để các xử lý business logic trong Controller? Ta thử hình dung, bây giờ ứng dụng MyContact của ta phải cung cấp RESTful Web Service cho một app Android => ta phải viết thêm một RestController. Nếu như không tách riêng thành phần Service thì trong RestController ta bắt buộc phải viết lại các thao tác của Controller đã có. Điều này đã phá vỡ nguyên tắc DRY (Don't Repeat Yourself).

Dưới đây mình sẽ giải thích rõ hơn workflow của ứng dụng thông qua use case Người dùng truy cập vào trang Danh sách liên hệ:

  • Spring sẽ quét tất cả các Controller, lựa chọn phương thức có URI Pattern phù hợp với URI trên trình duyệt của người dùng
  • Phương thức này sẽ gọi Service để lấy danh sách liên hệ từ CSDL
  • Service không trực tiếp truy cập vào CSDL mà sẽ nhờ Repository lấy tất cả liên hệ trong CSDL.
  • Repository trả về cho Service một danh sách liên hệ tìm thấy, Service lại chuyển danh sách đó cho Controller, rồi Controller sẽ gắn danh sách đó vào một đối tượng Model để đổ ra View

Đến đây, chắc các bạn cũng hiểu sơ qua về kiến trúc của ứng dụng. OK, giờ thì bắt tay vào code thôi!

Áp dụng Spring Framework vào dự án

Khởi tạo dự án

Với công cụ Spring Tool Suite, chúng ta có thể nhanh chóng tạo 1 dự án Spring bằng Spring Boot như sau:

Đầu tiên, ta chọn File -> New -> Other:
alt text

Gõ từ khóa spring, chọn Spring Starter Project rồi chọn Next:
alt text

Sau đó nhập các thông tin cơ bản như Name, Group, Artifact, Package. Các bạn chú ý Name không được có dấu cách nhé. Ở đây mình sẽ chọn Type là Maven và Java Version là 8:
alt text

Sau đó, chọn Next. Spring Boot sẽ yêu cầu chọn các dependency cho dự án. Ở đây, mình sẽ chọn các dependency sau:

  • Web ở trong nhóm Web
  • JPA và MySQL trong nhóm SQL
  • Thymeleaf trong nhóm Template Engines

Sau đó chọn Next:
alt text

Chọn Finish:
alt text

Qúa trình khởi tạo dự án bắt đầu. Spring Boot sẽ tự động tải các dependency mà ta đã chọn ở trên và tiến hành cấu hình:
alt text

Đây là cây thư mục của dự án sau khi khởi tạo xong:
alt text

File pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>

    <groupId>com.yuen.spring</groupId>
    <artifactId>mycontact</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>SpringMyContact</name>
    <description>MyContact app using Spring Framework</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.4.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

Ta thấy cấu hình chung của dự án sẽ gói gọn trong file SpringMyContactApplication.java:

package com.yuen;

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

SpringBootApplication
public class SpringMyContactApplication {

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

Đơn giản hơn rất nhiều so với cách cấu hình thông thường. Cho ta cảm tưởng như đang viết hàm của một ứng dụng console vậy :D


Xây dựng View

Spring Boot và Thymeleaf

Nếu chỉ sử dụng Spring MVC trong dự án thì chúng ta cần phải cấu hình Thymeleaf thì mới có thể sử dụng được template engine này. Nhưng với Spring Boot, bởi vì chúng ta đã thêm dependency spring-boot-starter-thymeleaf ở trong file pom.xml nên Spring Boot sẽ tự động cấu hình Thymeleaf.

Mặc định, Spring Boot sẽ đọc các file template từ thư mục templates trong src/main/resources. Nên ta sẽ bỏ các file html vào thư mục này.

Để không phải relaunch server để xem các thay đổi trên giao diện, các bạn vào file application.properties thêm cấu hình sau:

# ===============================
# THYMELEAF
# ===============================
spring.thymeleaf.cache=false

Sau này chúng ta chỉ cần resfresh trình duyệt để xem các thay đổi trên giao diện thay vì phải relaunch server.

Quản lý resources

Các file static resources (css, js, ảnh) sẽ được Spring Boot đọc từ thư mục static trong src/main/resources. Để cho tiện quản lý:

  • Các file css được đặt trong static/css
  • Các file js được đặt trong static/js
  • Các file ảnh được đặt trong static/images

Giả sử ta có một file style.css trong static/css thì thẻ <link> sẽ là:

<link href="../static/css/style.css"
      th:href="@{css/style.css}" rel="stylesheet" />
  • Thuộc tính href là của HTML5, cung cấp đường dẫn tới file style.css cho trình duyệt nếu như server chưa run. Cho nên chúng ta hoàn toàn có thể mở file html để xem dù chưa run server.
  • Thuộc tính th:href là của Thymeleaf, cung cấp đường dẫn tới file style.css cho trình duyệt khi server run. @{} mà một biểu thức SPeL xác định đường dẫn.

Vậy với các thư viện như Bootstrap, jQuery ... thì ta phải làm như thế nào? OK, đã có WebJars. WebJars là các thư viện phía client được đóng gói vào trong các file JAR. Hiện tại thì Spring Boot cũng đã hỗ trợ WebJars, nên ta chỉ cần thêm các dependency mong muốn vào file pom.xml.

Trong dự án này, mình sử dụng Bootstrap 3.3.7 và jQuery 1.12.4 nên chúng ta sẽ thêm vào pom.xml như sau:

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>bootstrap</artifactId>
    <version>3.3.7</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>1.12.4</version>
</dependency>

Sau khi lưu file pom.xml, Eclipse sẽ tự động tải các dependency mới thêm này về.

Thẻ <link><script> của ta sẽ như sau:

<link
    href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
    th:href="@{/webjars/bootstrap/3.3.7/css/bootstrap.min.css}"
    rel="stylesheet" />`

<script 
    src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js" 
    th:src="@{/webjars/jquery/1.12.4/jquery.min.js}"></script>

<script 
    src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
    th:src="@{/webjars/bootstrap/3.3.7/js/bootstrap.min.js}"></script>

Vì các file WebJars không nằm trong thư mục static nên mình sẽ sử dụng link CDN cho thuộc tính href. Nhìn đường dẫn ở th:href chắc các bạn có vẻ bối rối :D Không sao, mình sẽ chỉ cho các bạn một cách đơn giản để xác định đường dẫn này:
Ở cây thư mục dự án, bạn mở Maven Dependencies, tìm đến bootstrap-3.3.7.jar. Đường dẫn cần tìm sẽ tính từ thư mục webjars nhé:
alt text

Xây dựng layout

Ta thấy cả 2 file list.htmlform.html đều có 3 phần chung là head (thẻ head), header (thẻ nav) và footer (thẻ footer) nên mình sẽ giữ 3 phần chung này gộp vào 1 file layout.html đặt trong thư mục templates. Và đánh dấu mỗi phần bằng một thuộc tính th:fragment riêng:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head">
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->

<title>Spring MyContact</title>

<link href="../static/images/logo.png" th:href="@{/images/logo.png}"
    rel="shortcut icon" />

<!-- Bootstrap -->
<link
    href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
    th:href="@{/webjars/bootstrap/3.3.7/css/bootstrap.min.css}"
    rel="stylesheet" />
<!-- Custom style -->
<link href="../static/css/style.css" th:href="@{/css/style.css}"
    rel="stylesheet" />

<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script
    src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"
    th:src="@{/webjars/jquery/1.12.4/jquery.min.js}"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script
    src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
    th:src="@{/webjars/bootstrap/3.3.7/js/bootstrap.min.js}"></script>

<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
      <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
</head>
<body>
    <nav th:fragment="header"
        class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed"
                    data-toggle="collapse" data-target="#navbar" aria-expanded="false"
                    aria-controls="navbar">
                    <span class="sr-only">Toggle navigation</span> 
                    <span class="icon-bar"></span> 
                    <span class="icon-bar"></span> 
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">Spring MyContact</a>
            </div>
        </div>
    </nav>

    <h1>Main content</h1>

    <footer th:fragment="footer" class="container"> 
        &copy; 2017 Yuen 
    </footer>
</body>
</html>

Lúc này ở 2 file list.htmlform.html, mình sẽ thay thế các phần head, header và footer như sau:

<head th:replace="layout :: head"></head>
<nav th:replace="layout :: header"></nav>
<footer th:replace="layout :: footer"></footer>

Trong đó:

  • layout là tham chiếu tới file layout.html
  • head, headerfooter sau :: là các fragment selector, chính là giá trị của thuộc tính th:fragment của các thẻ head, navfooter ở file layout.html

File list.html lúc này:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="layout :: head"></head>
<body>
    <nav th:replace="layout :: header"></nav>

    <div class="container main-content list">
        <div class="row">
            <a href="#" class="btn btn-success pull-left">
                <span class="glyphicon glyphicon-plus"></span> Add new contact
            </a>
            <form class="form-inline pull-right" action="#" method="GET">
                <div class="form-group">
                    <input type="text" class="form-control" name="q" placeholder="Type contact name..." />
                </div>
                <button type="submit" class="btn btn-primary">Search</button>
            </form>
        </div>
        <div class="row">
            <table class="table table-bordered table-hover">
                <thead>
                    <tr>
                        <th>No</th>
                        <th>Name</th>
                        <th>Email</th>
                        <th>Phone</th>
                        <th>Edit</th>
                        <th>Delete</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>1</td>
                        <td>Nguyen Tuan Anh</td>
                        <td>nguyentuananh11b6@gmail.com</td>
                        <td>0123456789</td>
                        <td><a href="#"><span class="glyphicon glyphicon-pencil"></span></a></td>
                        <td><a href="#"><span class="glyphicon glyphicon-trash"></span></a></td>
                    </tr>
                    <tr>
                        <td>1</td>
                        <td>Nguyen Tuan Anh</td>
                        <td>nguyentuananh11b6@gmail.com</td>
                        <td>0123456789</td>
                        <td><a href="#"><span class="glyphicon glyphicon-pencil"></span></a></td>
                        <td><a href="#"><span class="glyphicon glyphicon-trash"></span></a></td>
                    </tr>
                    <tr>
                        <td>1</td>
                        <td>Nguyen Tuan Anh</td>
                        <td>nguyentuananh11b6@gmail.com</td>
                        <td>0123456789</td>
                        <td><a href="#"><span class="glyphicon glyphicon-pencil"></span></a></td>
                        <td><a href="#"><span class="glyphicon glyphicon-trash"></span></a></td>
                    </tr>
                    <tr>
                        <td>1</td>
                        <td>Nguyen Tuan Anh</td>
                        <td>nguyentuananh11b6@gmail.com</td>
                        <td>0123456789</td>
                        <td><a href="#"><span class="glyphicon glyphicon-pencil"></span></a></td>
                        <td><a href="#"><span class="glyphicon glyphicon-trash"></span></a></td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
    <!-- /.container -->

    <footer th:replace="layout :: footer"></footer>
</body>
</html>

File form.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="layout :: head"></head>
<body>
    <nav th:replace="layout :: header"></nav>

    <div class="container main-content form">
        <div class="row">
          <form action="#" method="POST">
            <div class="form-group">
              <label>Name</label>
              <input type="text" class="form-control" />
            </div>
            <div class="form-group">
              <label>Email</label>
              <input type="email" class="form-control" />
            </div>
            <div class="form-group">
              <label>Phone</label>
              <input type="text" class="form-control" />
            </div>
            <button type="submit" class="btn btn-primary">Save</button>
          </form>
        </div>
    </div><!-- /.container -->

    <footer th:replace="layout :: footer"></footer>
</body>
</html>


Xây dựng DataAccess layer và Service layer

Trước khi bắt tay vào code các thành phần ở lớp DataAccess, mình sẽ nói qua các khái niệm sau:

  • JPA (viết tắt của Java Persistence API) là một công nghệ cho phép chúng ta ánh xạ các POJO sang CSDL quan hệ và ngược lại. Các bạn hãy nhớ rằng JPA chỉ là 1 API (Application Programming Interface). Vì đã là interface thì chúng ta cần phải có implementation để implement interface đó.
  • Hibernate là một trong những implementation phổ biến nhất của JPA.
  • Spring Data JPA là một dự án trong gia đình Spring Data. Spring Data JPA giúp chúng ta tiết kiệm thời gian trong qúa trình tương tác với CSDL đồng thời cũng viết ít code hơn. Spring Data JPA implement Repository Pattern. Nếu bạn không muốn dùng Spring Data JPA thì có thể dùng DAO Pattern. DAO Pattern rất giống Repository Pattern.

Khi chúng ta thêm dependency Spring Data JPA vào file pom.xml thì Hibernate mặc định cũng được thêm vào.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Cấu hình DataSource, JPA, Hibernate

Các cấu hình liên quan đến DataSource, JPA, Hibernate chúng ta sẽ viết trong file application.properties của java/main/resources. Các bạn có thể tham khảo cấu hình sau:

# ===============================
# DATASOURCE
# ===============================

# Set here configurations for the database connection

# Connection url for the database "mycontact"
spring.datasource.url=jdbc:mysql://localhost:3306/mycontact?useSSL=false

# MySQL username and password 
spring.datasource.username=root
spring.datasource.password=root

# Keep the connection alive if idle for a long time (needed in production)
spring.datasource.dbcp.test-while-idle=true
spring.datasource.dbcp.validation-query=SELECT 1

# ===============================
# JPA / HIBERNATE
# ===============================

# Use spring.jpa.properties.* for Hibernate native properties (the prefix is
# stripped before adding them to the entity manager).

# Show or not log for each sql query
spring.jpa.show-sql=true

# Hibernate ddl auto (create, create-drop, update): with "update" the database
# schema will be automatically updated accordingly to java entities found in
# the project
spring.jpa.hibernate.ddl-auto=update

# Naming strategy
spring.jpa.hibernate.naming.strategy=org.hibernate.cfg.ImprovedNamingStrategy

# Allows Hibernate to generate SQL optimized for a particular DBMS
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect

Domain

Đây là các POJO ánh xạ từ các bảng ở CSDL. Mình sẽ tạo 1 lớp Contact.java trong package domain của src/main/java:

package com.yuen.domain;

import java.io.Serializable;

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 org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;

@Entity
@Table(name = "contact")
public class Contact implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", nullable = false)
    private int id;

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

    @Column(name = "email")
    private String email;

    @Column(name = "phone")
    private String phone;

    public Contact() {
        super();
    }

    public Contact(int id, String name, String email, String phone) {
        super();
        this.id = id;
        this.name = name;
        this.email = email;
        this.phone = phone;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

}

Lớp Contact các bạn chú ý phải implements Serializable và có các getter/setter cho các thuộc tính để sau này chúng ta có thể truy xuất các thuộc tính trong View.

Các annotation mình sử dụng trong đoạn code trên là các annotation của JPA:

  • @Entity xác định lớp hiện tại là một entity (entity là một thành phần của JPA, chính là POJO mà mình nói ở trên)
  • @Table xác định tên bảng ánh xạ sang
  • @Id xác định thuộc tính hiện tại là ID trong bảng CSDL
  • @GeneratedValue xác định kiểu sinh khóa chính, ở đây là AUTO_INCREMENT
  • @Column xác định thuộc tính hiện tại là một cột trong CSDL

Repository

Đây là các interface giúp chúng ta thao tác với CSDL. Trong Spring Data JPA có một tính năng rất hay đó là CRUD Repository. Đây là một interface cung cấp các phương thức CRUD cơ bản. Chúng ta chỉ cần định nghĩa một interface kế thừa CRUD Repository, Spring Data JPA sẽ dùng các generic và reflection để sinh implementation tương ứng với interface đó.

Do CRUD Repository mới chỉ cung cấp các phương thức cơ bản để đọc, lưu, xóa... nên giả sử bạn muốn tìm kiếm các liên hệ theo một query nào đó thì bạn phải sử dụng đến Query builder. Cơ chế này sẽ dựa theo tên của phương thức để xây dựng câu truy vấn. Mình thấy có 2 trang khá hay để hiểu hơn về Query Builder:

  1. https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation.
  2. https://www.petrikainulainen.net/programming/spring-framework/spring-data-jpa-tutorial-introduction-to-query-methods/

OK, bây giờ mình sẽ viết một interface ContactRepository trong package com.yuen.repository:

package com.yuen.repository;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

import com.yuen.domain.Contact;

public interface ContactRepository extends CrudRepository<Contact, Integer> {

    List<Contact> findByNameContaining(String q);

}

Trong đó:

  • <Contact, Integer> chỉ ra kiểu dữ liệu của entity và entity ID.
  • findByNameContaining(String q) là phương thức tìm kiếm liên hệ có name LIKE %name%

Service

Lớp này sẽ phụ trách các xử lý business logic trong ứng dụng. Ta sẽ xây dựng một interface có tên ContactService trong package com.yuen.service chứa các phương thức sẽ sử dụng trong Controller:

package com.yuen.service;

import java.util.List;

import com.yuen.domain.Contact;

public interface ContactService {

    Iterable<Contact> findAll();

    List<Contact> search(String q);

    Contact findOne(int id);

    void save(Contact contact);

    void delete(int id);

}

Bây giờ ta cần phải cài đặt implementation cho inteface ContactService. Sau này mình sẽ autowire ContactService ở trong Controller. Chắc các bạn thắc mắc tại sao mình lại autowire interface mà không autowire class, viết thêm interface làm chi cho mất công thì bạn có thể tìm kiếm câu trả lời tại đây nhé: http://stackoverflow.com/questions/5288153/using-spring-to-wire-directly-a-concrete-class

Trong package com.yuen.service, mình sẽ viết một class ContactServiceImpl.java implement ContactService:

package com.yuen.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.yuen.domain.Contact;
import com.yuen.repository.ContactRepository;

@Service
public class ContactServiceImpl implements ContactService {

    @Autowired
    private ContactRepository contactRepository;

    @Override
    public Iterable<Contact> findAll() {
        return contactRepository.findAll();
    }

    @Override
    public List<Contact> search(String q) {
        return contactRepository.findByNameContaining(q);
    }

    @Override
    public Contact findOne(int id) {
        return contactRepository.findOne(id);
    }

    @Override
    public void save(Contact contact) {
        contactRepository.save(contact);
    }

    @Override
    public void delete(int id) {
        contactRepository.delete(id);
    }

}

Trong đó:

  • Annotation @Service giúp Spring xác định lớp hiện tại là một Service.
  • Annotation @Autowired dùng để inject ContactRepository vào ContactService.

OK như vậy là đã xong phần DataAccess Layer và Service Layer.


Xử lý các use case

Hiển thị danh sách liên hệ

+ Controller

Trước tiên, trong package com.yuen.controller, mình sẽ tạo lớp ContactController.java, đây là Controller duy nhất trong ứng dụng MyContact:

package com.yuen.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import com.yuen.service.ContactService;

@Controller
public class ContactController {

    @Autowired
    private ContactService contactService;

    @GetMapping("/contact")
    public String index(Model model) {
        model.addAttribute("contacts", contactService.findAll());
        return "list";
    }

}

Trong đó:

  • Annotation @Controller giúp Spring xác định lớp hiện tại là một Controller.
  • Annotation GetMapping xác định phương thức index() sẽ đón nhận các request có HTTP method là GET và URI pattern là /contact.
  • Annotation @Autowired inject ContactService vào ContactController
  • Phương thức index() được truyền vào 1 tham số có kiểu dữ liệu là Model. Model có nhiệm vụ truyền dữ liệu từ Controller cho View. Ở đây mình sẽ lấy ra danh sách các liên hệ thông qua contactService.findAll(). Sau đó gắn danh sách này vào Model thông qua phương thức addAttribute(), contacts chính là tên biến đại diện cho danh sách mà ta sẽ dùng ở View sau này.
  • Phương thức index() trả về 1 String, từ String này Spring sẽ suy ra View nào sẽ nhận dữ liệu từ Controller: return "list";" => view là list.html

+ View

Trước khi đổ dữ liệu ra View thì mình sẽ kiểm tra xem contacts có rỗng không:

<th:block th:if="${#lists.isEmpty(contacts)}">
    <h3>No contacts</h3>
</th:block>

Trong đó:

  • #list là một đối tượng tiện ích (Utility Object) trong Thymeleafs để xử lý các dữ liệu kiểu danh sách. Phương thức isEmpty() trả về true nếu contacts rỗng và ngược lại.
  • Thuộc tính th:if tương ứng với "câu lệnh if" => trái ngược với th:if là thẻ th:unless (not if).
  • Ta sẽ bao đóng toàn bộ đoạn code này bằng thẻ th:block. Nếu như điều kiện trong th:if không thỏa mãn thì toàn bộ nội dung của th:block sẽ biến mất trong source file HTML.

Trong trường hợp contacts không rỗng thì mình sẽ đổ dữ liệu ra View như sau:

<th:block th:unless="${#lists.isEmpty(contacts)}">
    <div class="row">
        <a href="#" class="btn btn-success pull-left">
            <span class="glyphicon glyphicon-plus"></span> Add new contact
        </a>
        <form class="form-inline pull-right" action="#" method="GET">
                    <div class="form-group">
                        <input type="text" class="form-control" name="q"
                            placeholder="Type contact name..." />
                    </div>
                    <button type="submit" class="btn btn-primary">Search</button>
                </form>
            </div>
            <div class="row">
                <table class="table table-bordered table-hover">
                    <thead>
                        <tr>
                            <th>No</th>
                            <th>Name</th>
                            <th>Email</th>
                            <th>Phone</th>
                            <th>Edit</th>
                            <th>Delete</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr th:each="contact,iterStat : ${contacts}">
                            <td th:text="${iterStat.count}"></td>
                            <td th:text="${contact.name}"></td>
                            <td th:text="${contact.email}"></td>
                            <td th:text="${contact.phone}"></td>
                            <td><a href="#"><span class="glyphicon glyphicon-pencil"></span></a></td>
                            <td><a href="#"><span class="glyphicon glyphicon-trash"></span></a></td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </th:block>

Trong đó:

  • Thuộc tính th:each tương ứng với "câu lệnh foreach"
  • Thuộc tính th:text dùng để đổ dữ liệu dưới dạng text vào thẻ HTML
  • ${contacts} là một iterated variable, chính là đối tượng contacts truyền từ Controller
  • contact là một iteration variable
  • iterStat là một status variable giúp chúng ta theo dõi vòng lặp, biến count sẽ lấy ra chỉ số hiện tại của vòng lặp (bắt đầu từ 1)

File list.html sau use case này:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="layout :: head"></head>
<body>
    <nav th:replace="layout :: header"></nav>

    <div class="container main-content list">
        <th:block th:if="${#lists.isEmpty(contacts)}">
            <h3>No contacts</h3>
        </th:block>

        <th:block th:unless="${#lists.isEmpty(contacts)}">
            <div class="row">
                <a href="#" class="btn btn-success pull-left"><span
                    class="glyphicon glyphicon-plus"></span> Add new contact</a>
                <form class="form-inline pull-right">
                    <div class="form-group">
                        <input type="text" class="form-control" name="criteria"
                            placeholder="Type contact name..." />
                    </div>
                    <button type="submit" class="btn btn-primary">Search</button>
                </form>
            </div>
            <div class="row">
                <table class="table table-bordered table-hover">
                    <thead>
                        <tr>
                            <th>NO</th>
                            <th>Name</th>
                            <th>Email</th>
                            <th>Phone</th>
                            <th>Edit</th>
                            <th>Delete</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr th:each="contact,iterStat : ${contacts}">
                            <td th:text="${iterStat.count}"></td>
                            <td th:text="${contact.name}"></td>
                            <td th:text="${contact.email}"></td>
                            <td th:text="${contact.phone}"></td>
                            <td><a href="#"><span class="glyphicon glyphicon-pencil"></span></a></td>
                            <td><a href="#"><span class="glyphicon glyphicon-trash"></span></a></td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </th:block>
    </div>
    <!-- /.container -->

    <footer th:replace="layout :: footer"></footer>
</body>
</html>


Thêm liên hệ mới

+ Hiển thị form

Trước hết ta cần phải sửa lại đường dẫn cho button Add new contactlist.html để điều hướng tới trang Thêm liên hệ:

<a th:href="@{/contact/create}" class="btn btn-success pull-left">
    <span class="glyphicon glyphicon-plus"></span> Add new contact
</a>

Ở class ContactController, ta viết thêm 1 phương thức create() để hiển thị form liên hệ:

@GetMapping("/contact/create")
public String create(Model model) {
    model.addAttribute("contact", new Contact());
    return "form";
}

Trong đó:

  • Giá trị trong @GetMapping tương ứng với đường link của button Add new contact
  • Mình sẽ truyền sang form.html một đối tượng Contact có tên là contact. Mỗi thuộc tính của contact sẽ tương ứng với một input trong form.

Thẻ form trong form.html sẽ được viết lại như sau:

<form action="#" th:action="@{/contact/save}" th:object="${contact}"
    method="POST" novalidate="novalidate">
    <input type="hidden" th:field="*{id}" />
    <div class="form-group">
        <label>Name</label> 
        <input type="text" class="form-control"
            th:field="*{name}" th:errorclass="field-error" /> 
        <em th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></em>
    </div>
    <div class="form-group">
        <label>Email</label> 
        <input type="email" class="form-control" 
            th:field="*{email}" th:errorclass="field-error" /> 
        <em th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></em>
    </div>
    <div class="form-group">
        <label>Phone</label> 
        <input type="text" class="form-control" th:field="*{phone}" />
    </div>
    <button type="submit" class="btn btn-primary">Save</button>
</form>

Trong đó:

  • th:action chỉ ra đường dẫn sẽ xử lý submit form, ${contact}th:object chính là biến contact. Do mình sẽ sử dụng Spring validation nên sẽ để form này novalidate.
  • Như mình đã nói ở trên, mỗi thuộc tính của contact sẽ tương ứng với một input trong form. Nên mình sẽ thêm thuộc tính th:field=*{fieldName} vào các input. Do khi submit form, chúng ta cần phải chỉ cho Hibernate biết entity nào được gửi lên, nên mình phải thêm <input type="hidden" th:field="*{id}" /> để chỉ ra ID của entity.
  • <em th:if="${#fields.hasErrors('fieldName')}" th:errors="*{fieldName}"></em> dùng để hiển thị validation messages nếu field đó invalid. Về validation messages, mình sẽ giải thích ngay trong phần dưới đây.

Các bạn có thể tham khảo thêm cách viết form tại đây

+ Cấu hình validation
Trong use case này, mình sẽ yêu cầu người dùng:

  • Không được bỏ trống tên liên hệ
  • Email phải đúng định dạng email

Đầu tiên ở class Contact.java, mình sẽ sử dụng 2 validator constraint @NotEmpty@NotEmail của Hibernate để xác định field nào cần validate và validate cái gì:

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

@Email
@Column(name = "email")
private String email;

Chúng ta có 2 cách để khai báo validation message:

  • Khai báo ngay tại thuộc tính message của validator constraint
  • Khai báo trong một file properties theo quy tắc: constraint.domain.field=validationMessage

Trong tut này, mình sẽ hướng dẫn các bạn sử dụng cách 2. Cách này tuy lằng nhằng hơn cách 1 nhưng sẽ giúp chúng ta quản lý các message dễ hơn:

Đầu tiên ta tạo 1 file messages.properties ngay dưới thư mục src/main/resources. Mình khuyên các bạn nên cài công cụ Properties Editor, công cụ này sẽ hỗ trợ chúng ta tạo cũng như viết tiếng Việt file properties trong Eclipse.

NotEmpty.contact.name=Vui lòng nhập tên cho liên hệ
Email.contact.email=Vui lòng nhập đúng định dạng email

Chưa xong, chúng ta cần phải thêm một chút cấu hình để Spring MVC có thể load được file properties này. Ta tạo class WebMvcConfig.java trong package com.yuen.config:

package com.yuen.config;

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource bean = new ReloadableResourceBundleMessageSource();
        bean.addBasenames("classpath:messages");
        return bean;
    }

}

Trong đó:

  • Annotation @Configuration xác định đây là một class dùng để cấu hình
  • classpath:messages chỉ ra đường dẫn tới file mesages.properties

+ Xử lý form submit

Bước này gồm 2 công đoạn là validation và lưu contact. Tạo phương thức save() trong ContactController.java:

@PostMapping("/contact/save")
public String save(@Valid Contact contact, BindingResult result, RedirectAttributes redirect) {
    if (result.hasErrors()) {
        return "form";
    }
    contactService.save(contact);
    redirect.addFlashAttribute("success", "Saved contact successfully!");
    return "redirect:/contact";
}
  • Do request gửi lên có HTTP method là POST nên mình sẽ sử dụng PostMapping
  • Đối tượng contact được truyền vào save() chính là đối tượng contact mà mình đã truyền từ create() sang form. Đối tượng này sẽ lưu thông tin mà người dùng nhập vào.
  • Để bật cơ chế validation cho contact, mình sẽ sử dụng annotation @Valid. Các validation message bắn ra sẽ được đối tượng result bắt.
    • Nếu như có lỗi thì mình sẽ quay trở lại form để thông báo lỗi đó
    • Còn không thì sẽ lưu contact. Sau đó mình sẽ redirect về trang danh sách liên hệ. Chuỗi đằng sau "redirect:" là đường dẫn của trang mà mình muốn redirect. Đồng thời mình cũng gửi một flash message về trang danh sách liên hệ để thông báo lưu thành công, bằng cách sử dụng: redirect.addFlashAttribute(messageName, messageContent)

Ở file list.html, bạn thêm đoạn code sau để hiển thị flash message:

<div class="container main-content list">
    <div th:if="${success}" class="row alert alert-success alert-dismissible">
        <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        <span th:text="${success}"></span>
    </div>
    ...
</div>


Sửa liên hệ

Đầu tiên ở file list.html, ta cần phải sửa lại đường dẫn để điều hướng tới trang Sửa liên hệ:

<td><a th:href="@{/contact/{id}/edit(id=${contact.id})}"><span class="glyphicon glyphicon-pencil"></span></a></td>

Các bạn có thể thao khảo thêm cách viết đường dẫn trong Thymleaf tại đây

Do use case này cũng khá giống với thêm liên hệ mới, nên ta chỉ cần viết thêm một phương thức edit() để hiển thị form sửa liên hệ:

@GetMapping("/contact/{id}/edit")
public String edit(@PathVariable int id, Model model) {
    model.addAttribute("contact", contactService.findOne(id));
    return "form";
}

Tham số @PathVariable int id lấy ID của liên hệ từ đường dẫn rồi gán vào một biến id. Từ id mình sẽ tìm được liên hệ bằng phương thức findOne() của ContactService, rồi cũng truyền sang form.html.


Xóa liên hệ

Đầu tiên ở file list.html, ta cần phải sửa lại đường dẫn để điều hướng tới trang Xóa liên hệ:

<td><a th:href="@{/contact/{id}/delete(id=${contact.id})}"><span class="glyphicon glyphicon-trash"></span></a></td>

Tạo phương thức delete() trong ContactController.java:

@GetMapping("/contact/{id}/delete")
public String delete(@PathVariable int id, RedirectAttributes redirect) {
    contactService.delete(id);
    redirect.addFlashAttribute("success", "Deleted contact successfully!");
    return "redirect:/contact";
 }


Tìm kiếm liên hệ

Ta sửa lại form tìm kiếm như sau:

<form class="form-inline pull-right" action="#" th:action="@{/contact/search}" method="GET">
    <div class="form-group">
        <input type="text" class="form-control" name="q" placeholder="Type contact name..." />
    </div>
    <button type="submit" class="btn btn-primary">Search</button>
</form>

Trong ContactController.java, viết thêm phương thức search() để xử lý form submit:

@GetMapping("/contact/search")
public String search(@RequestParam("q") String q, Model model) {
    if (q.equals("")) {
        return "redirect:/contact";
    }

    model.addAttribute("contacts", contactService.search(q));
    return "list";
}

Do form tìm kiếm có method là GET nên giá trị của input sẽ hiển thị trên URL. Để lấy các tham số trên URL, ta sử dụng annotation @RequestParam, cách khai báo cũng tương tự như annotation @PathVariable.

Nếu như chuỗi tìm kiếm rỗng, mình sẽ redirect luôn về trang chủ, còn không thì sẽ tìm kiếm các liên hệ bằng phương thức search() của contactService.



Đây là cây thư mục cuối cùng của dự án:
alt text

Chạy dự án

Để chạy dự án, ta sẽ chuột phải tên dự án, rồi chọn Run As -> Spring Boot App:
alt text

Lúc này dự án của chúng ta sẽ chạy trên cổng 8080: http://localhost:8080/contact

Các bạn chú ý là do Tomcat đã nhúng trực tiếp vào Spring Boot nên ta không cần phải bật Tomcat để chạy ứng dụng.

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Nguyễn Tuấn Anh

9 bài viết.
77 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
20 11
Hướng dẫn lập trình Spring Security Trong bài viết lần này, mình sẽ giúp các bạn bước đầu tìm hiểu (Link) thông qua xây dựng các chức năng: Đăng ...
Nguyễn Tuấn Anh viết hơn 1 năm trước
20 11
White
12 0
Giới thiệu Spring Framework Trong bài viết này, mình sẽ giới thiệu cho các bạn về một trong những Java EE framework rất nổi bật và phổ biến hiện ...
Nguyễn Tuấn Anh viết hơn 1 năm trước
12 0
White
7 5
Viết ứng dụng đầu tiên với Spring Trong bài viết này, mình sẽ hướng dẫn các bạn viết ứng dụng HelloWorld kinh điển trong Spring. Các công cụ mình...
Nguyễn Tuấn Anh viết hơn 1 năm trước
7 5
Bài viết liên quan
White
8 0
Trong phần cuối cùng này chúng ta sẽ cùng nói về Spring Data Access, Aspect Oriented Programming (AOP), Spring MVC. Spring Data Access 42. Sử dụn...
Hoàng Nguyễn viết hơn 2 năm trước
8 0
White
20 11
Hướng dẫn lập trình Spring Security Trong bài viết lần này, mình sẽ giúp các bạn bước đầu tìm hiểu (Link) thông qua xây dựng các chức năng: Đăng ...
Nguyễn Tuấn Anh viết hơn 1 năm trước
20 11
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
9 bài viết.
77 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!