เริ่มโค้ด Microservice part 1: Monolith
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 หรือพูดว่า ก้อนเดียวกัน
หรือทำให้ชัดขึ้นอีก
HCare Project
ผมจะสาธิตโดยสร้างโปรเจกต์ชื่อ hcare ทำหน้าที่บันทึกจำนวนชั่วโมงทำงานของพนักงานแต่ละคนเพื่อคำนวณเป็นค่าจ้างในแต่ละเดือน มันไม่ได้เขียนจนแล้วเสร็จทั้งหมดแต่ก็เพียงพอสำหรับนำมาเป็นตัวอย่าง
ทฤษฎีนำตามด้วยปฏิบัติ โปรเจกต์ต่อไปนี้ผมจะเขียนด้วย Spring Boot ใช้ภาษา Java, JDK 1.8 และ Maven เป็น build tool
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 เรื่องย่อย ได้แก่
- การคำนวณจำนวนชั่วโมงระหว่างวันเวลาที่เริ่ม (start) กับวันเวลาที่สิ้นสุด (end)
- จำนวนชั่วโมงทั้งหมด (total) ของทั้งเดือน
- การคำนวณจำนวนเงินที่จะต้องจ่ายแก่พนักงานแต่ละคน (พนักงานแต่ละคนตกลงค่าจ้างไม่เท่ากัน)
domain หรือ business model จึงประกอบด้วย 2 คลาสหลักคือ Employee และ ManDate
คลาส 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
คำนวณออกมาได้ว่า
มือใหม่ต้องรู้ว่าโปรเจกต์นี้เขียนด้วย concept หรือความคิดของ Spring Boot MVC และ CRUD โดยปกติแล้วเมื่อมี domain (model, business model) หน้าตาดังข้างต้นก็จะมี controller, service และ reposiotry มารับผิดชอบดังภาพ
ความสัมพันธ์
- 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
<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
<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
<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
ผล
ข้อดูข้อมูลพนักงานที่มี id เท่ากับ 1
GET localhost:8080/api/employees/1
ผล
ขอสร้างข้อมูลพนักงานเพิ่ม 1 คน
POST localhost:8080/api/employees
พร้อมข้อมูล
{"firstName": "สมปอง","lastName": "ทองสุข"}
ผล
ขอดูข้อมูล man date ทั้งหมดของพนักงานที่มี id เท่ากับ 2 (เพิ่งสร้างใหม่)
GET localhost:8080/api/mandates/employees/2
ผล
ดังนั้นขอเพิ่มข้อมูล 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"}
ผล
จากนั้นขอดูข้อมูล man date ทั้งหมดของพนักงานที่มี id เท่ากับ 2 อีกครั้ง
ผล
ขอลบข้อมูล man date ของพนักงานที่มี id เท่ากับ 2
DELETE localhost:8080/api/mandates/4/employees/2
ผล
เพื่อนๆจะเห็นว่า rest controller ใช้ API หรือ URI เป็น interface คั่นกลางระหว่าง UI กับ business logic ซึ่งสามารถสร้างเพิ่มเติมได้อีก
ด้วยเหตุนี้การเติบโตของโปรเจกต์ในทิศทางของ rest controller จะตัดขาดจาก UI เดิมอย่าง Thymeleaf ออกไปอย่างสิ้นเชิงแล้วมอบประสบการณ์ใหม่แก่ platform อื่นที่ต้องการใช้งาน business logic เดียวกันนี้
เพื่อ 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 ที่นี่