เริ่มโค้ด Microservice part 1: Monolith

Phai Panda
8 min readAug 4, 2020

--

monolith และ microservice เป็นสถาปัตยกรรม, monolith พูดว่าก้อนเดียวกัน ต่างจาก microservice ที่ต้องการเอาชนะเรื่องปริมาณการใช้งานข้อมูลโดยการ scale

ออกตัวก่อนว่าผมไม่ได้เก่งเรื่องสถาปัตยกรรม (architecture) ทั้งยังไม่มีประสบการณ์การพัฒนา microservice มาก่อน แต่ผมโชคดีที่ได้ดูแลชิ้นงานซึ่งความตั้งใจเดิมของผู้สร้างต้องการให้มันเป็น microservice นี่จึงเป็นที่มา เป็นความท้าทาย นอกจากจะได้แสวงหาความรู้แล้วยังได้แบ่งปันประสบการณ์นับจากนี้ของผมแก่มือใหม่ที่สนใจ microservice ทว่าไม่รู้จะเริ่มต้นโค้ดได้ยังไง

ย้อนไปก่อนหน้านี้ไม่นานนักผมและทีมต้องการ implement เรื่องนี้โดยการตั้งคำถามง่ายๆว่า “จะเริ่มเขียนโค้ดจากความเข้าใจจุดไหน” โชคร้ายที่คำตอบที่ได้ไม่เปิดโอกาสให้ลงมือทำเลย ติดคำถามที่ว่า

  • เทคโนโลยีที่ใช้มาสุดทางแล้วหรือยังก่อนจะเปลี่ยนสถาปัตยกรรมจาก monolith เป็น microservice
  • เข้าใจและมีความรู้เรื่องคอขวดที่เกิดตรงจุดแล้วหรือไม่ คำว่าช้าหรือปริมาณข้อมูลที่ต้องการจะ scale นั้นเท่าไร อะไรบ้าง
  • ทราบได้อย่างไรว่า microservice จะช่วยเรื่องความช้าที่ฐานข้อมูลเพราะมันเป็นช่องทางเดียวที่สถาปัตยกรรม monolith ส่ง request จำนวนมากเข้าไปจนพังนั่นได้ ข้อเท็จจริงนี้ถูกต้องแล้วหรือไม่

เอาล่ะไม่ว่าตอนนั้นจะติดที่คำถามไหนก็ตาม เมื่อต้องการรู้จึงต้องศึกษาให้ทราบความ อย่างน้อยพบ keywords สำคัญไปค้นคว้าต่อก็ยังดีกว่าไม่ลงมือทำอะไรเลย บทความนี้ผมจึงตั้งใจเขียนเพื่อให้เกิดการจุดประกาย เพื่อเป็นอีกช่องทางหนึ่งในการศึกษา microservice ครับ

รู้จัก Monolith

ในความคิดผมมือใหม่ต้องรู้จัก monolith ก่อนจะไปรู้จัก microservice เพราะ microservice เกิดขึ้นจากปัญหาของ monolith ที่ต้องการการ scale ทั้งในแง่ปริมาณการใช้งานข้อมูลและทรัพยากรที่มีความหลากหลาย

เมื่อให้สี่เหลี่ยมต่อไปนี้เป็น component ทั้ง front end, back end และ database ถูกจับมาอยู่รวมกัน การออกแบบสถาปัตยกรรมแบบนี้เรียกว่า monolith หรือพูดว่า ก้อนเดียวกัน

monolith #1

หรือทำให้ชัดขึ้นอีก

monolith #2

HCare Project

ผมจะสาธิตโดยสร้างโปรเจกต์ชื่อ hcare ทำหน้าที่บันทึกจำนวนชั่วโมงทำงานของพนักงานแต่ละคนเพื่อคำนวณเป็นค่าจ้างในแต่ละเดือน มันไม่ได้เขียนจนแล้วเสร็จทั้งหมดแต่ก็เพียงพอสำหรับนำมาเป็นตัวอย่าง

ทฤษฎีนำตามด้วยปฏิบัติ โปรเจกต์ต่อไปนี้ผมจะเขียนด้วย Spring Boot ใช้ภาษา Java, JDK 1.8 และ Maven เป็น build tool

by https://start.spring.io/

Spring Boot Dependencies

ที่เลือกได้แก่

  • Spring Web
  • Thymeleaf
  • Spring Data JPA
  • H2 Database
  • Lombok

Project Structure

application.properties

#H2 database web console
spring.h2.console.enabled=true
#H2 database file
spring.datasource.url=jdbc:h2:file:~/hcare_db;DB_CLOSE_ON_EXIT=FALSE;
#generate database if not there
spring.jpa.hibernate.ddl-auto=update
#show sql in console
spring.jpa.properties.hibernate.show_sql=true

หัวใจ

หัวใจของ business คือการคำนวณจำนวนชั่วโมงระหว่างวันเวลาที่เริ่มกับวันเวลาที่สิ้นสุด เช่น ทำงานวันนี้ตั้งแต่ 8 โมงเช้าถึงบ่ายสามโมงครึ่ง แล้วนำจำนวนชั่วโมงทั้งหมดของทั้งเดือนมาคำนวณเป็นจำนวนเงินที่จะต้องจ่ายแก่พนักงานแต่ละคน

ขอแยกเป็น 3 เรื่องย่อย ได้แก่

  1. การคำนวณจำนวนชั่วโมงระหว่างวันเวลาที่เริ่ม (start) กับวันเวลาที่สิ้นสุด (end)
  2. จำนวนชั่วโมงทั้งหมด (total) ของทั้งเดือน
  3. การคำนวณจำนวนเงินที่จะต้องจ่ายแก่พนักงานแต่ละคน (พนักงานแต่ละคนตกลงค่าจ้างไม่เท่ากัน)

domain หรือ business model จึงประกอบด้วย 2 คลาสหลักคือ Employee และ ManDate

data model

คลาส Employee

package com.pros.exam.hcare.model;

import lombok.Data;

import javax.persistence.*;
import java.util.List;

@Data
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "employeeId")
private List<ManDate> manDateList;
}

คลาส ManDate

package com.pros.exam.hcare.model;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.Date;

@Data
@Entity
public class ManDate {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Date start;
private Date end;
private Long employeeId;
}

ตัวอย่างเมื่อจัดเก็บข้อมูลใน H2

employee table
man_date table

คำนวณออกมาได้ว่า

จำนวนชั่วโมงทำงานรวมทั้งหมด

มือใหม่ต้องรู้ว่าโปรเจกต์นี้เขียนด้วย concept หรือความคิดของ Spring Boot MVC และ CRUD โดยปกติแล้วเมื่อมี domain (model, business model) หน้าตาดังข้างต้นก็จะมี controller, service และ reposiotry มารับผิดชอบดังภาพ

monolith #3

ความสัมพันธ์

  • EmployeeController, EmployeeService (Impl) และ EmployeeRepository
  • ManDateController, ManDateService (Impl) และ ManDateRepository

คลาส EmployeeController

package com.pros.exam.hcare.controller;

import com.pros.exam.hcare.model.Employee;
import com.pros.exam.hcare.service.EmployeeService;
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 org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class EmployeeController {
private EmployeeService employeeService;

@Autowired
public EmployeeController(EmployeeService employeeService) {
this.employeeService = employeeService;
}

@GetMapping("/employees/create")
public String index(Model model) {
model.addAttribute("employee", new Employee());

return "create-employee";
}

@PostMapping("/employees/create")
public String create(@ModelAttribute(value = "employee") Employee employee) {
employeeService.create(employee);

return "redirect:/";
}
}

คลาส ManDateController

package com.pros.exam.hcare.controller;

import com.pros.exam.hcare.model.Employee;
import com.pros.exam.hcare.model.ManDate;
import com.pros.exam.hcare.model.ManDateForm;
import com.pros.exam.hcare.service.EmployeeService;
import com.pros.exam.hcare.service.ManDateService;
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 org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Comparator;
import java.util.List;

@Controller
public class ManDateController {
private EmployeeService employeeService;
private ManDateService manDateService;

@Autowired
public ManDateController(
EmployeeService employeeService,
ManDateService manDateService
) {
this.employeeService = employeeService;
this.manDateService = manDateService;
}

@GetMapping("/mandates/employees/{employeeId}")
public String index(@PathVariable("employeeId") Long employeeId, Model model) {
return toIndexPage(employeeId, model);
}

@GetMapping("/mandates/employees/{employeeId}/create")
public String toCreatePage(@PathVariable("employeeId") Long employeeId, Model model) {
Employee employee = employeeService.findById(employeeId);

model.addAttribute("employee", employee);
model.addAttribute("manDateForm", new ManDateForm());

return "create-mandate";
}

@PostMapping("/mandates/employees/{employeeId}/create")
public String create(@ModelAttribute(value = "manDateForm") ManDateForm manDateForm, Model model) throws ParseException {
Long employeeId = manDateForm.getEmployeeId();

ManDate manDate = new ManDate();
manDate.setEmployeeId(employeeId);
manDate.setStart(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm").parse(manDateForm.getStart()));
manDate.setEnd(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm").parse(manDateForm.getEnd()));
manDateService.create(employeeId, manDate);

return toIndexPage(employeeId, model);
}

@GetMapping("/mandates/{mandateId}/employees/{employeeId}/delete")
public String toDeletePage(@PathVariable("employeeId") Long employeeId, Model model) {
return toIndexPage(employeeId, model);
}

@PostMapping("/mandates/{mandateId}/employees/{employeeId}/delete")
public String delete(@PathVariable("mandateId") Long manDateId, @PathVariable("employeeId") Long employeeId, Model model) {
manDateService.delete(employeeId, manDateId);

return toIndexPage(employeeId, model);
}

private String toIndexPage(Long employeeId, Model model) {
Employee employee = employeeService.findById(employeeId);
List<ManDate> list = manDateService.findAllByEmployeeId(employee.getId());
list.sort(Comparator.comparing((o -> ((ManDate)o).getStart())).reversed());

model.addAttribute("employee", employee);
model.addAttribute("manDateList", list);

return "mandate";
}
}

คลาส EmployeeServiceImpl

package com.pros.exam.hcare.service;

import com.pros.exam.hcare.exception.EmployeeIdMustBeNullException;
import com.pros.exam.hcare.exception.EmployeeNotFoundException;
import com.pros.exam.hcare.model.Employee;
import com.pros.exam.hcare.repository.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

@Service
public class EmployeeServiceImpl implements EmployeeService {
private EmployeeRepository repository;

@Autowired
public EmployeeServiceImpl(EmployeeRepository repository) {
this.repository = repository;
}

@Override
public List<Employee> findAll() {
return StreamSupport.stream(repository.findAll().spliterator(), false).collect(Collectors.toList());
}

@Override
public Employee findById(Long id) {
return repository.findById(id).orElseThrow(() -> new EmployeeNotFoundException("No employee id " + id + " found"));
}

@Override
public Employee create(Employee employee) {
if(employee.getId() != null) {
throw new EmployeeIdMustBeNullException("Employee ID must be null");
}
return repository.save(employee);
}
}

คลาส ManDateServiceImpl

package com.pros.exam.hcare.service;

import com.pros.exam.hcare.model.ManDate;
import com.pros.exam.hcare.repository.ManDateRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Service
public class ManDateServiceImpl implements ManDateService {
private ManDateRepository repository;

@Autowired
public ManDateServiceImpl(ManDateRepository repository) {
this.repository = repository;
}

@Override
public List<ManDate> findAllByEmployeeId(Long employeeId) {
return repository.findAllByEmployeeId(employeeId).orElse(new ArrayList<>());
}

@Override
public ManDate create(Long employeeId, ManDate manDate) {
manDate.setEmployeeId(employeeId);
return repository.save(manDate);
}

@Transactional
@Override
public void delete(Long employeeId, Long manDateId) {
repository.deleteByIdAndEmployeeId(manDateId, employeeId);
}
}

คลาส EmployeeRepository

package com.pros.exam.hcare.repository;

import com.pros.exam.hcare.model.Employee;
import org.springframework.data.repository.CrudRepository;

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

คลาส ManDateRepository

package com.pros.exam.hcare.repository;

import com.pros.exam.hcare.model.ManDate;
import org.springframework.data.repository.CrudRepository;

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

public interface ManDateRepository extends CrudRepository<ManDate, Long> {
Optional<List<ManDate>> findAllByEmployeeId(Long employeeId);

void deleteByIdAndEmployeeId(Long manDateId, Long employeeId);
}

เนื่องจากว่าหน้าแรก (index.html) มีการคำนวณจำนวนชั่วโมงทั้งหมดซึ่งจะไม่เก็บไว้ในฐานข้อมูล การคำนวณนี้จึงได้แยกเป็น service ใหม่ ให้ชื่อว่า EmployeeInHourService มีบริการสำคัญชื่อ getTotal

ความสัมพันธ์

  • EmployeeInHourService (Impl) และ HomeController

คลาส EmployeeInHourServiceImpl

package com.pros.exam.hcare.service;

import com.pros.exam.hcare.model.Employee;
import com.pros.exam.hcare.model.ManDate;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;

@Service
public class EmployeeInHourServiceImpl implements EmployeeInHourService {
private ManDateService manDateService;

public EmployeeInHourServiceImpl(ManDateService manDateService) {
this.manDateService = manDateService;
}

@Override
public double getTotal(Employee employee) {
List<ManDate> list = manDateService.findAllByEmployeeId(employee.getId());
return list.stream().mapToDouble(manDate -> {
LocalDateTime start = manDate.getStart().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
LocalDateTime end = manDate.getEnd().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
Duration duration = Duration.between(start, end);
return duration.toMinutes() / 60.0;
}).sum();
}
}

EmployeeInHourService อยู่คู่กับหน้าแรก

คลาส HomeController

package com.pros.exam.hcare.controller;

import com.pros.exam.hcare.model.EmployeeInHour;
import com.pros.exam.hcare.service.EmployeeInHourService;
import com.pros.exam.hcare.service.EmployeeService;
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 java.util.List;
import java.util.stream.Collectors;

@Controller
public class HomeController {
private EmployeeService employeeService;
private EmployeeInHourService employeeInHourService;

@Autowired
public HomeController(
EmployeeService employeeService,
EmployeeInHourService employeeInHourService
) {
this.employeeService = employeeService;
this.employeeInHourService = employeeInHourService;
}

@GetMapping("/")
public String index(Model model) {
List<EmployeeInHour> list = employeeService.findAll().stream().map(employee ->
new EmployeeInHour(employee, employeeInHourService.getTotal(employee))
).collect(Collectors.toList());
model.addAttribute("employeeInHourList", list);

return "index";
}
}

มาดู HTML templates กันบ้าง ผมนำมาแสดงเฉพาะไฟล์สำคัญ ตัดมาเฉพาะบางส่วนของหน้า ได้แก่

  • index.html
  • mandate.html
  • create-mandate.html

หน้า index.html

index.html, table part
<table class="table">
<thead>
<tr>
<th>ลำดับ</th>
<th>ชื่อ</th>
<th>นามสกุล</th>
<th class="text-right">ชั่วโมง</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="item: ${employeeInHourList}">
<td th:text="${itemStat.index + 1}" />
<td th:text="${item.employee.firstName}" />
<td th:text="${item.employee.lastName}" />
<td class="text-right" th:text="${#numbers.formatDecimal(item.total, 1, 'COMMA', 2, 'POINT')}"/>
<td>
<a class="btn btn-primary" th:href="@{/mandates/employees/{employeeId}(employeeId=${item.employee.id})}">เพิ่มชั่วโมง</a>
</td>
</tr>
</tbody>
</table>

ข้อสังเกต ตัวแปร employeeInHourList เป็นชนิด List<EmployeeInHour> สร้างเพื่อใช้เป็น model คุยกับ view ผ่าน controller (HomeController)

หน้า mandate.html

mandate.html, table part
<table class="table">
<thead>
<tr>
<th>ลำดับ</th>
<th>เริ่ม</th>
<th>สิ้นสุด</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="item: ${manDateList}">
<td th:text="${itemStat.index + 1}" />
<td th:text="${#dates.format(item.start, 'dd-MM-yyyy HH:mm')}" />
<td th:text="${#dates.format(item.end, 'dd-MM-yyyy HH:mm')}" />
<td>
<form action="#" th:action="@{/mandates/{mandateId}/employees/{employeeId}/delete(mandateId=${item.id},employeeId=${employee.id})}" method="post">
<input type="submit" class="btn btn-primary" value="ลบ" />
</form>
</td>
</tr>
</tbody>
</table>

หน้า create-mandate.html

create-mandate.html, form part
<form action="#" th:action="@{/mandates/employees/{employeeId}/create(employeeId=${employee.id})}" th:object="${manDateForm}" method="post">
<input type="hidden" id="employeeId" value="${employee.id}" />
<div class="d-flex justify-content-between">
<div class="form-group">
<label for="start">เริ่ม</label>
<input type="datetime-local" class="form-control" id="start" th:field="*{start}">
</div>
<div class="form-group">
<label for="end">สิ้นสุด</label>
<input type="datetime-local" class="form-control" id="end" th:field="*{end}">
</div>
</div>
<div>
<input type="submit" class="btn btn-primary" value="เพิ่ม" />
<input type="reset" class="btn btn-secondary" value="ล้างค่า" />
</div>
</form>

ข้อสังเกต create-mandate.html ผมใช้ manDateForm เป็นออบเจ็กต์คุยระหว่าง view กับ model ผ่าน controller (ManDateController) นั่นเพราะผมรับค่า datetime-local เป็นสตริงจากนั้นจึงนำมาแปลงเป็นค่าของ java.util.Date ในภายหลัง

คลาส EmployeeInHour

package com.pros.exam.hcare.model;

import lombok.AllArgsConstructor;
import lombok.Data;

@AllArgsConstructor
@Data
public class EmployeeInHour {
private Employee employee;
private double total;
}

คลาส ManDateForm

package com.pros.exam.hcare.model;

import lombok.Data;

@Data
public class ManDateForm {
private Long id;
private String start;
private String end;
private Long employeeId;
}

ปัญหาและการแก้ไข

เพื่อนๆคงเห็นแล้วว่าความคิดของ monolith จะผูกพัน UI, business logic (service) และ database ไว้ใน environment เดียวกัน จากตัวอย่างนี้เขียนด้วย Spring Boot ใช้ภาษา Java เป็นหลักคุย model, view และ controller (MVC)

ความซับซ้อนของโปรเจกต์จะเพิ่มภาระไปที่ controller จะต้องบริหารจัดการ URL ส่วน view จะหนักไปที่ jQuery ซึ่งเป็นที่ทราบกันดีในสมัยนี้ว่าล้าหลังกว่าพวก front end framework อย่าง Angular, Vue และ library อย่าง React มาก

สมมติเราต้องการพัฒนา UI ใหม่บน platform ของ Android และ iOS ความคิดของ controller ข้างต้นนี้ก็ใช้ไม่ได้แล้วครับเพราะมันผูกพันธ์กับ Thymeleaf ชนิดที่ว่าเป็นเนื้อเดียวกัน แยกออกจากกันไม่ได้ หรือถ้าเราต้องการ front end framework ใหม่มาทำงาน business logic นี้ก็ทำไม่ได้เหมือนกัน

เราจึงต้องแสวงหาความคิดใหม่มาจัดการเรื่องนี้อย่างมีหลักการ หนึ่งในนั้นเรียกว่า RESTful Web Services

RESTful Web Services ที่ผมจะ implement หรือเขียนโค้ดให้ดูนี้จะต้องทำให้ controller ที่เรารู้จักเป็นมากกว่า controller ธรรมดา พูดว่าเพิ่มความสามารถเข้าไปอีก ข่าวดีคือ Spring Boot มี @RestController รองรับสิ่งนี้

rest controller นั้นผมสร้างแยกไว้ใน package ชื่อ restcontroller ประกอบด้วย

  • EmployeeRestController
  • ManDateRestController

คลาส EmployeeRestController

package com.pros.exam.hcare.restcontroller;

import com.pros.exam.hcare.model.Employee;
import com.pros.exam.hcare.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;

@RestController
public class EmployeeRestController {
private EmployeeService service;

@Autowired
public EmployeeRestController(EmployeeService service) {
this.service = service;
}

@GetMapping("/api/employees")
public ResponseEntity findAll() {
return ResponseEntity.ok(service.findAll());
}

@GetMapping("/api/employees/{id}")
public ResponseEntity findById(@PathVariable(name = "id") Long id) {
return ResponseEntity.ok(service.findById(id));
}

@PostMapping("/api/employees")
public ResponseEntity create(@RequestBody Employee employee) {
Employee newEmployee = service.create(employee);
return ResponseEntity.created(URI.create("/api/employees/" + newEmployee.getId())).build();
}
}

คลาส ManDateRestController

package com.pros.exam.hcare.restcontroller;

import com.pros.exam.hcare.model.ManDate;
import com.pros.exam.hcare.model.ManDateForm;
import com.pros.exam.hcare.service.ManDateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.List;

@RestController
public class ManDateRestController {
private ManDateService manDateService;

@Autowired
public ManDateRestController(ManDateService manDateService) {
this.manDateService = manDateService;
}

@GetMapping("/api/mandates/employees/{employeeId}")
public ResponseEntity findAll(@PathVariable("employeeId") Long employeeId) {
List list = manDateService.findAllByEmployeeId(employeeId);
return ResponseEntity.ok(list);
}

@PostMapping("/api/mandates/employees/{employeeId}")
public ResponseEntity create(@PathVariable("employeeId") Long employeeId, @RequestBody ManDateForm manDateForm) throws ParseException {
ManDate manDate = new ManDate();
manDate.setEmployeeId(employeeId);
manDate.setStart(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm").parse(manDateForm.getStart()));
manDate.setEnd(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm").parse(manDateForm.getEnd()));

ManDate newManDate = manDateService.create(employeeId, manDate);
return ResponseEntity.created(URI.create("/api/mandates/employees/" + employeeId)).build();
}

@DeleteMapping("/api/mandates/{mandateId}/employees/{employeeId}")
public ResponseEntity delete(@PathVariable("mandateId") Long manDateId, @PathVariable("employeeId") Long employeeId) {
manDateService.delete(employeeId, manDateId);
return ResponseEntity.ok().build();
}
}

ทดสอบ Rest Controller

ขอดูรายการข้อมูลพนักงานทั้งหมด

GET localhost:8080/api/employees

ผล

GET all employees

ข้อดูข้อมูลพนักงานที่มี id เท่ากับ 1

GET localhost:8080/api/employees/1

ผล

GET employee id 1

ขอสร้างข้อมูลพนักงานเพิ่ม 1 คน

POST localhost:8080/api/employees

พร้อมข้อมูล

{"firstName": "สมปอง","lastName": "ทองสุข"}

ผล

POST new employee

ขอดูข้อมูล man date ทั้งหมดของพนักงานที่มี id เท่ากับ 2 (เพิ่งสร้างใหม่)

GET localhost:8080/api/mandates/employees/2

ผล

empty man date array

ดังนั้นขอเพิ่มข้อมูล man date แก่พนักงานที่มี id เท่ากับ 2

POST localhost:8080/api/mandates/employees/2

พร้อมข้อมูล

{"start": "2020-08-02T01:00:00.000+00:00","end": "2020-08-02T11:30:00.000+00:00"}

ผล

POST new man date to employee id 2

จากนั้นขอดูข้อมูล man date ทั้งหมดของพนักงานที่มี id เท่ากับ 2 อีกครั้ง

ผล

GET all man dates of employee id 2

ขอลบข้อมูล man date ของพนักงานที่มี id เท่ากับ 2

DELETE localhost:8080/api/mandates/4/employees/2

ผล

DELETE man date id 4 of employee id 2

เพื่อนๆจะเห็นว่า rest controller ใช้ API หรือ URI เป็น interface คั่นกลางระหว่าง UI กับ business logic ซึ่งสามารถสร้างเพิ่มเติมได้อีก

ด้วยเหตุนี้การเติบโตของโปรเจกต์ในทิศทางของ rest controller จะตัดขาดจาก UI เดิมอย่าง Thymeleaf ออกไปอย่างสิ้นเชิงแล้วมอบประสบการณ์ใหม่แก่ platform อื่นที่ต้องการใช้งาน business logic เดียวกันนี้

client & server using rest api communication

เพื่อ implement ภาพข้างต้นนี้ ตัวอย่างใน part ถัดไปผมสร้าง UI โดยใช้ Vue แทน Thymeleaf

สรุป

สถาปัตยกรรม monolith ให้ภาพของ front end, back end และ database อยู่ร่วมกันและถึงแม้จะแยก front end ซึ่งในที่นี้คือ UI หรือ view ออกไปเป็น RESTful Web Services หรือ API แล้วก็ยังคงลักษณะของความเป็น monolith ไม่เปลี่ยนแปลงครับ

อ่านต่อ

อ้างอิง

softnix.co.th, Microservices vs Monolithic ที่นี่

medium.com, สรุป Microservice vs. Monolith ที่นี่

learn-it-with-examples.com, UPDATING data using Spring & Thymeleaf ที่นี่

javaguides.net, Spring Boot Thymeleaf CRUD Example Tutorial ที่นี่

https://frontbackend.com/thymeleaf/thymeleaf-utility-methods-for-numbers

https://www.dariawan.com/tutorials/spring/spring-boot-thymeleaf-crud-example/

stackoverflow.com, How to format the currency in HTML5 with thymeleaf ที่นี่

--

--

No responses yet