Spring Boot with RESTful Web Services 2/3
ใช้ Spring Boot เขียน RESTful Web Services หัวใจคือ Spring Boot Starter Data JPA เชื่อมต่อกับ Oracle Database 12c และ Spring Boot Starter Web
ผมใช้ Docker และได้ติดตั้ง Oracle Database 12c จึงถึงคราวลองเอามันมาใช้นะครับ อ่านได้ที่
ถัดจากนั้นได้สร้างโปรเจกต์ Spring Boot ด้วย Gradle จัดหา Oracle Driver ที่รู้จัก Oracle Database 12c และใช้ Maven ติดตั้งมันก่อนจะดึงเข้าโปรเจกต์อีกที
เหตุที่ว่า Oracle มีเครื่องมือที่เรียกว่า SQL*Plus (ขอเขียนง่ายๆว่า sqlplus) จึงอยากทดลองใช้ กลับไปติดที่ password ที่ผมตั้งนั้นประกอบด้วยเครื่องหมาย @ ซึ่งทำให้ไม่สามารถเข้าใช้ sqlplus ใน container ได้ ฉะนั้นแก้ไขครับ
และล่าสุดกับ Spring Boot with RESTful Web Services 1/3 หากเพื่อนๆยังไม่ได้อ่านก็แวะเข้าไปอ่านก่อนนะ
เตรียมพร้อม
- เรามีโปรเจกต์ Gradle ที่เขียนด้วย Spring Boot กับ Spring Boot Starter Data JPA
- โปรเจกต์นี้ผูกด้วย Oracle Database Driver
- โปรเจกต์นี้สามารถ run ทดสอบได้โดยไม่มี error
- เรามีความรู้ความเข้าใจเบื้องต้นเกี่ยวกับ RESTful Web Services
สิ่งที่จะทำ
- จะสร้าง RESTful Web Services
- โปรเจกต์นี้ชื่อ my_books (ของเพื่อนๆก็แล้วแต่ชอบ) ดังนั้นมันต้องเกี่ยวกับหนังสือ (ตอนนี้ยังไม่ได้คิดถึงทิศทาง)
- ขึ้นโครงสร้างโปรเจกต์ด้วย MVC ย่อมาจาก Model View Controller
รู้จักกับ Spring Boot Starter
ที่ผ่านมาผมก็พาทำไม่ได้เอาทฤษฏีมาจับมากเพราะกลัวว่าตนเองจะเบื่อ ยอมรับว่าอ่านที่มาที่ไปของเรื่องนี่มันไม่สนุกเท่าเปิดดูโค้ดชาวบ้าน (ขำ) สุกเอาเผากินอาหารไม่ย่อยฉันใด ท้องของเราก็จะไม่สบายฉันนั้น งั้นเรามาเรียนรู้ไปพร้อมๆกันนะครับ
เรื่องนี้เริ่มต้นที่ lib เขียนเต็มๆคือ library ต่างๆที่โปรเจกต์จำเป็นต้องมี ขาดไม่ได้ เพราะขาดหรือเวอร์ชันไม่เข้ากันเมื่อไรโปรเจกต์มักพัง รันไม่ขึ้น เดือดร้อน
ภาษาจาวาก็จะมีไฟล์นามสกุล .jar นี่แหละเป็น library กล่าวคือด้านในประกอบด้วยไฟล์นามสกุล .class จำนวนหนึ่งซึ่งมีคำเรียกอย่างเป็นทางการว่า Java Class Library ย่อว่า JCL และก็มาลึกเกินไป
อ่านเพิ่มเติม
ความที่โปรเจกต์อาศัย JCL หลายตัว (หลาย .jar) ปัญหาที่ตามมาคือความเข้ากันได้ครับ คือมันค่อนข้างจะเข้ากันไม่ได้โดยเฉพาะกับเลขเวอร์ชันเล็กๆที่อยู่ในลำดับถัดๆไป ตัวอย่างเช่น X.1.3 อาจเข้ากันไม่ได้กับ Y.5.1.2 แต่เข้ากันได้กับ Y.5.1.1 เป็นต้น
เข้าเรื่องเลย จะดีกว่าไหมหากมีคนจัดการเรื่องเหล่านี้ให้ คือการันตีว่าพวกมันสามารถทำงานร่วมกันได้อย่างลงตัว จึงเป็นที่มาของประดา Spring Boot Starter ครับ ประโยชน์ที่ได้อีกอย่างคือเขาได้รวบรวมสิ่งที่ใช้บ่อยและแยกเป็นเรื่องไว้อย่างชัดเจน อยากทำ ORM ติดต่อกับฐานข้อมูลเหรอ Spring Boot Starter Data JPA สิ อยากสร้างเว็บกับ RESTful Web Services เหรอ Spring Boot Starter Web สิ
แล้วทั้งหมดมีกี่ Starter ไปดูกัน
เอาล่ะกลับมาที่โปรเจกต์เดิมก่อนหน้านี้ ทบทวนนะ เปิดไฟล์ build.gradle
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:2.1.6.RELEASE")
}
}
apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group 'com.pros'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
compile("org.springframework.boot:spring-boot-starter-data-jpa")
compile group: 'com.oracle', name: 'ojdbc7', version: '12.1.0.2'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
เปิดไฟล์ application.properties มีการเปลี่ยนแปลงดังนี้
#spring.jpa.hibernate.ddl-auto=create
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:ORCLCDB
spring.datasource.username=SYSTEM
spring.datasource.password=Password1234
spring.datasource.driver-class-oracle.jdbc.driver.OracleDriver
ไฟล์นี้ก็อย่างที่เพื่อนๆเข้าใจได้ มีรายละเอียดที่น่าสนใจดังนี้
- spring.jpa.hibernate.ddl-auto แท้จริงคือ hibernate.hbm2ddl.auto คือเป็นเรื่องของ Hibernate Framework ว่าด้วยการจัดการโครงสร้างของตาราง ได้แก่
none
,validate
,update
,create
, และcreate-drop
- spring.jpa.hibernate.ddl-auto=create คือใช้สร้างตาราง โดยมากจะกำหนดไว้ครั้งแรกที่เริ่มทำงานโปรแกรม (เพราะตารางยังไม่เกิดขึ้น) ทว่าหากได้สร้างตารางในฐานข้อมูลไว้แล้ว ค่านี้จะแก้ไขเป็น update
- spring.datasource.url กำหนดว่าใช้ Java Database Connectivity (JDBC) เชื่อมต่อกับฐานข้อมูลชื่ออะไร
- spring.datasource.username คือ username เข้าฐานข้อมูล
- spring.datasource.password คือ password เข้าฐานข้อมูล
- spring.datasource.driver-class-xxx คือ ชื่อที่ผ่านการรับรองและ JDBC รู้จัก
อย่างไหนที่เพื่อนๆอยากเข้าใจเพิ่ม ใช้คำหรือคีย์เวิร์ดที่ผมให้ไว้ค้นหาด้วย Google นะครับ ส่วนด้านล่างนี้คือ common-application-properties
อ่านเพิ่มเติม
Model View and Controller
เขียน Spring Boot เพื่อนๆก็ต้องรู้จักภาษาจาวามาบ้างอยู่แล้ว เพื่อนๆรู้จักคลาสในจาวา (model) เพื่อนๆรู้จักหน้าเว็บ (view) และตอนนี้เพื่อนๆจะรู้ว่ามีตัวควบคุมคลาสกับหน้าเว็บให้คุยกันด้วย (controller)
Models
ว่าด้วยเรื่อง Business Models หรือจะเรียกว่า Domain ของเรื่องก็ได้ เป็นส่วนที่สำคัญมากที่สุด (ได้เงินก็ตรงนี้แหละ) ตัวอย่างเช่น โปรเจกต์คิดเกรดนักเรียน ก็อาจประกอบไปด้วยคลาส Student และ Subject
สำหรับ Spring Framework จะเขียน Model นี้ร่วมกับความคิดของ Plain Old Java Object (POJO) คือเป็นจาวาคลาสธรรมดา (ที่สุดในโลก) ถูกนำมาใช้ตั้งแต่ EJB เวอร์ชัน 3.0 และสิ่งที่ POJO ไม่ควรทำเลยคือ
- ไม่สืบทอดคลาส กล่าวคืออยู่ของมันโดดๆนั่นแหละ ตัวอย่างเช่น
class Student { … } อันนี้เป็น POJO
class StudentHttpServlet extends javax.servlet.http.HttpServlet { … } อันนี้ไม่เป็น POJO - ไม่ใช้ Interfaces กล่าวคืออยู่ของมันโดดๆนั่นแหละ
- ไม่ใช้ Annotations กล่าวคืออยู่ของมันโดดๆนั่นแหละ
แต่ชีวิตเราก็ใช่ว่าจะตรงเผงขนาดปากซอยจะไปต้องเรียกวินมอไซค์อย่างเดียว เรียกแก๊ปมอไซค์ก็ได้
Spring Boot ก็มี Java Bean มาช่วยในเรื่องนี้ กล่าวคือข้อกำหนดใดที่เป็นมากกว่า POJO แต่ยังอยู่ในขอบเขตของการสร้างออบเจ็กต์ (Object) ล่ะก็ Java Bean จะทำ ส่วนรายละเอียดเชิงลึกขอละไปก่อนนะ
Views
คิดถึงหน้าเว็บทั่วๆไป จะใช้เทคโนโลยีอะไรมาทำก็ตามใจชอบ แต่ในที่นี้เราจะทำง่ายที่สุด โดย response กลับไปเป็น JSON
Controllers
concept อย่างแรกคือจัดการ URLs ว่าด้วยเรื่องเส้นทางเข้าถึง resources ในแบบต่างๆไม่ว่าจะเป็น GET, POST, PUT และ DELETE (มีเพิ่มอีก ทว่าที่แบงก์นี่กะใช้แค่สองอย่างคือ GET กับ POST) อย่างที่สองคือจับ model ไปโยนใส่ view และหรือรับข้อมูลจาก view มากำหนดลง model นั่นก็แล้วแต่จะออกแบบกัน
หากศึกษาเรื่อง MVC ของ Spring Framework มาก่อน เพื่อนก็จะทราบว่าเขายังแบ่งชั้น (layer) ของการจัดการข้อมูลและ Business ออกเป็น Data Access Object (DAO) กับ Services ตามลำดับ ซึ่งผมขอไม่กล่าวถึงในตอนนี้
อ่านเพิ่มเติม
เห็นไหมว่ากว่าจะได้องค์ความรู้มาเขียนเป็นชิ้นงานจำต้องอ่านและศึกษาไม่น้อย เหตุเพราะมันเคยมีปัญหาเกิดขึ้นมาก่อน แล้วนักพัฒนาเราก็หาวิธีการมาแก้ไข ทั้งคิด concept ทั้งสร้าง framework ต่างๆนานา พวกเขาก็ถกเถียงและฝ่าฟันกันไม่น้อย
มาเริ่มกันเลย
เขียนจาวาย่อมต้องเข้าใจว่าคอกหรือการจัดกลุ่มจาวาคลาสเรียกว่า Packages ในที่นี้ผมจะสร้าง packages ตามนี้นะ
com.pros.modelcom.pros.repository
และมีคลาสกับ interface ตามนี้
com.pros.model.Book //book classcom.pros.repository.BookRepository //DAO layer for book interfacecom.pros.Application //include main method
Application Class
มาดูความตั้งใจของคลาส Application ก่อน ผมจะสร้างหนังสือมาหนึ่งเล่ม ชื่อว่า Java from Beginner ซึ่งสร้างได้แบบง่ายดายที่สุด
Book book1 = new Book();
จากนั้นจะกำหนดชื่อลงไป
book1.setName("Java from Beginner");
แล้วเอาไป insert ลงฐานข้อมูลด้วย repository หรือ DAO concept
bookRepository.save(book1);
ข้อดีของ JPA โดย Object-relational mapping (ORM) คือจาวาโปรแกรมเมอร์ไม่ต้องเขียน Data Manipulation Language (DML) เอง (ในที่นี้คือคำสั่ง INSERT ข้อมูล) คือปล่อยให้ความคิด ORM จัดการไปครับ
ทว่าจาวาโปรแกรมเมอร์ก็ยังพยายามเขียน DML เองควบคู่ไปกับ ORM concept ไม่ว่าจะเป็นเรื่องง่ายหรือยากในการจัดการข้อมูล กรณีแบบนี้จงปล่อยไปแล้วแต่ความสามารถในการดูแลรักษาโค้ด
ที่คลาส Application นี้ยังใช้ CommandLineRunner interface เพื่อบังคับให้เกิดการ execute โค้ดเพียงครั้งเดียวก่อนที่โปรแกรมนี้จะทำงานเสร็จ จริงๆผมเชื่อว่าเราอยากใช้ String… args ที่ส่งเข้ามา (ถ้าส่ง) มากกว่าการรันโค้ดไม่กี่บรรทัดซึ่งดูเหมือนไม่ได้ใช้ความสามารถนี้เลย ไม่เป็นไร ลอกตามชาวบ้านไปก่อน
public class Application implements CommandLineRunner { ... }
อ่านเพิ่มเติม
สรุปสิ่งที่ Application จะทำ
package com.pros;
import com.pros.model.Book;
import com.pros.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application implements CommandLineRunner {
@Autowired
private BookRepository bookRepository;
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
@Override
public void run(String... args) throws Exception {
Book book1 = new Book();
book1.setName("Java from Beginner");
bookRepository.save(book1);
}
}
หากว่ารันสำเร็จเราจะได้หนังสือหนึ่งเล่มโยนใส่ฐานข้อมูล นั่นไม่ไกลแล้ว อดทนอีกนิด
Book Class
เป็น model ว่าด้วยหนังสือหนึ่งเล่มนี้ควรมีอะไรบ้างเป็นส่วนประกอบ ง่ายดายที่สุดในตอนนี้ขอแค่ 2 อย่าง คือ id กับ name โดย id นั้นจะต้องเพิ่มขึ้นได้เอง (increment) จึงอาศัยวิธีการสร้างค่าแบบอัตโนมัติเรียกว่า Auto Increment อันนี้ก็แล้วแต่ว่าฐานข้อมูลแต่ละยี่ห้อนั้นได้จัดเตรียมไว้หรือไม่ เป็นเรื่องดีที่ Oracle Database มีให้จ้า
@GeneratedValue(strategy = GenerationType.AUTO)
อ่านเพิ่มเติม
ข้อกำหนดของ Java Bean ควรเขียน Setter และ Getter รวมถึง Default Constructor เสมอ
package com.pros.model;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Book {
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
private Long id;
private String name;
public Book() { }
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
BookRepository Interface
เมื่อก่อนนี่ผมคิดว่า framework นี่มันเวทมนต์เอามากๆ คือเราต้องเขียนและเตรียมโน่นนี่ไว้ในที่ที่มันกำหนดจึงจะทำงานได้เป็นปกติ
Spring Boot บอกว่า เฮ้เพื่อน ปกติแล้วเวลานาย insert ข้อมูลลงฐานข้อมูล นายอาจจะ update หรือ delete มันด้วยใช่ไหม สิ่งที่นายมักทำมีไม่เกิน 4 อย่างต่อไปนี้ ฉันพนันได้เลย!
- insert หรือ create ข้อมูล
- update ข้อมูล
- delete ข้อมูล
- query หรือ read ข้อมูล
ทำบ่อยๆก็กลายเป็น concept ได้ ข้างต้นนี้เรียกว่า CRUD ย่อมาจาก Create Read Update Delete เหตุนี้ framework จึงเตรียมให้ ด้วย interface ชื่อ CrudRepository
โครตเจ๋ง!
หนังสือหนึ่งเล่มใดๆต้องสามารถ สร้าง อ่าน แก้ไขและลบได้
package com.pros.repository;
import com.pros.model.Book;
import org.springframework.data.repository.CrudRepository;
public interface BookRepository extends CrudRepository<Book, Long> {
}
มา run กันเลย
BUILD SUCCESSFUL in 5s
โอเคร ทดสอบเท่านี้ก่อนว่ามันสำเร็จผล เราเดินมาครึ่งทาง ที่เหลือคือเขียน RESTful Web Services ซึ่งดูแล้วควรตัดเอาไปต่อใน part หน้า
อย่าลืมปรบมือให้กำลังใจตนเองนะครับ
อ่านต่อ part 3