Basic Spring Part 4 - Connect SQL Database

Basic Spring Part 4 - Connect SQL Database

เห็นช่วงนี้คนชอบเล่น surf skate เราก็อยากเล่นแต่อยากเล่นแบบนี้มากกว่า มันเรียกว่า lifting คือการเล่นคลื่นที่อยู่บนอากาศ ไม่มีในโลกแห่งความเป็นจริงอันใกล้ แต่มีในการ์ตูน

ผมเขียนเกี่ยวกับ Spring ไว้หลายตอน คุณสามารถกด Link ด้านล่างเพื่ออ่านที่เกี่ยวกับ Spring ตอนต่างๆได้เลย

สำหรับตอนนี้เราจะมาพูดถึงการต่อ Database กับนะครับว่าจะต่อ Database ยังไงต้องทำอะไรบ้าง สำหรับตอนนี้อาจจะใช้ Database อันนี้ผมแนะนำให้ใช้ Docker สร้าง Container ที่เป็น MySQL Database มาใช้งานนะครับ (เพราะง่าย) โดยวิธีการสร้างนั้นตาม Link นี้ไปเลย หรือใครไม่ถนัดก็สามารถ Install ตัว Mysql Server ได้เลยครับ

ในส่วนของ Code นั้นสามารถไปดูได้ที่ Github ได้เลย

Init Project

ทำการเข้า Web : https://start.spring.io/ จากนั้นทำการเลือกตามภาพด้านล่างและกด Generate

โดยตัวที่เราเพิ่มคือ Spring jpa data ซึ่งเป็น Lib ที่ทำให้เราเชื่อมต่อกับ SQL Database ได้ จากนั้นทำการเปิดด้วย IDE จากนั้นลองทำการ Start server ดูก็จะพบว่า Error ดังภาพซึ่งเกิดจากเราไม่ได้ทำการ Config ค่าในการเชื่อมต่อ Database

ดังนั้นจึงทำการ Config ก่อน โดยการ Config นั้นจะต้องทำที่ไฟล์ : src/main/resources/application.properties โดยต้องเพิ่ม properties ดังต่อไปนี้

1
2
3
4
5
6
# ตรงนี้คือ jdbc url คือ url ไปหาตัว database ซึ่ง database ของผมอยู่ที่ 192.168.56.101 port 3306 ซึ่งจะไม่เหมือนกับเครื่องของคุณแน่นอน
spring.datasource.url=jdbc:mysql://192.168.56.101:3306/webdb?useUnicode=yes&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&serverTimezone=Asia/Bangkok

# ตรงนี้คือ username password ในการเข้า database
spring.datasource.username=root
spring.datasource.password=root

เมื่อตั้งค่าเสร็จให้ลองทำการ Start server อีกครั้ง

ซึ่งจะพบว่า Error ใหม่ซึ่งเกี่ยวกับการหา Driver ไม่เจอซึ่งสามารถแก้ไขได้จากการเพิ่ม Dependency ที่เป็น Driver ของ mysql โดยไปเพิ่มที่ไฟล์ : pom.xml

1
2
3
4
5
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>

จากนั้นลองทำการ Start server อีกครั้ง ซึ่งคราวนี้จะพบว่าสามารถ Start server สำเร็จแล้ว

สร้าง Map Class กับ Table

ตัว Lib ที่เราใช้เชื่อมต่อกับ Database นั้นเราจะใช้ Lib : JPA โดย Concept ของ JPA คือเป็นตัวกลางที่จะแปลง Java Class ให้กลายเป็นข้อมูลที่จะไปอยู่ใน Table โดยตัว Lib จะแปลงข้อมูลเป็นคำสั่ง SQL ซึ่ง

  • ข้อดี

    • ง่ายในการเก็บข้อมูลไม่ต้องเขียน SQL เอง แค่ Set ค่าลงใส่ Object จากนั้นสั่ง Save ก็เป็นการ Insert หรือ Update ได้เลย เช่นกัน เวลาค้นหาข้อมูลก็จะดึงข้อมูลออกมาจาก Database มาใส่ใน Object ให้สามารถใช้งานได้เลย
    • สามารถเปลี่ยนตัว SQL Database เป็นยี่ห้ออื่นก็ได้ทันที เช่น สามารถเปลี่ยนจาก MYSQL เป็น MARIADB ได้เลยเพียงเปลี่ยน Config ไม่ต้องทำการแก้ Code เลย
  • ข้อเสีย

    • ไม่ยืดหยุ่นเท่าจัดการแบบ Native Query เพราะ JPA ต้องการความเป็นสามารถใช้ได้ทุก DB จึงไม่สามารถใช้ Function ที่เฉพาะเจาะจงกับ DB นั้นได้ (ทำได้แต่เปลี่ยน DB ต้องแก้ Code)
    • เวลาดึงข้อมูลนั้นจะดึงทุก Field ใน Table นั้นออกมาเพื่อ Map กับ Class ว่าง่ายๆมัน SELECT * เอา Field ที่ไม่ได้อยากใช้ออกมาด้วย ทำให้สิ้นเปลือง (สามารถดึงเฉพาะ Field ได้แต่ต้องสร้าง Class ใหม่)

Entity

Class ที่ใช้ Map กับตัว Table นั้นเราจะเรียกมันว่า Entity โดยเราจะลองสร้าง Class ที่ชื่อว่า People เก็บข้อมูลเกี่ยวกับบุคคล ดัง Code ข้างล่าง

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package test.spring.webdb.entity;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Getter
@Setter
@Accessors(chain = true)
@EqualsAndHashCode
@ToString
@Entity
@Table(name = "people")
public class People {

@Id
@Column(name = "ID")
private String id;

@Column(name = "FULL_NAME")
private String fullName;

@Column(name = "AGE")
private Integer age;

@Column(name = "ADDRESS")
private String address;
}
1
2
3
@Entity
@Table(name = "people")
public class People {

@Entity ส่วนนี้เป็นการบอกว่า Class นี้เป็น Entity

@Table เป็นการบอกว่า Enttiy นี้เชื่อมกับ Table ที่ชื่อ people

1
2
3
@Id
@Column(name = "ID")
private String id;

@Id ส่วนนี้บอกว่า Attribute นี้จะเป็น Primary key

@Column ส่วนนี้เป็นการบอกว่า Attribute นี้ผูกกับ Column ที่ชื่อ ID

Repository

Repository เป็น Service ที่เราใช้เชื่อมต่อกับ Database

1
2
3
4
5
6
7
8
package test.spring.webdb.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import test.spring.webdb.entity.People;

public interface PeopleRepository extends JpaRepository<People, String> {

}
1
public interface PeopleRepository extends JpaRepository<People, String>

ตรง JpaRepository<People, String> เป็นการบอกว่า Repository นี้จะใช้กับ Entity : People โดยมี Primary key เป็น type : String

ลองใช้งาน

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package test.spring.webdb.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import test.spring.webdb.entity.People;
import test.spring.webdb.repository.PeopleRepository;

import java.util.UUID;

@RestController
public class PeopleController {

@Autowired
private PeopleRepository peopleRepository;

@GetMapping("/test-create")
public void testCreate() {
People people = new People()
.setId(UUID.randomUUID().toString())
.setFullName("Normal Programmer")
.setAge(2)
.setAddress("In blog")
;
peopleRepository.save(people);
}
}

เราจะทำการสร้าง object : people แล้วทำการ save ค่าลง Database โดยใช้ peopleRepository ซึ่ง peopleRepository นั้นมาจากการ Inject เข้ามาโดยตัว Spring จะทำการสร้าง Object ให้เราโดยอัตโนมัติ คราวนี้เรามาลอง Start server และยิง Request ดูครับ ซึ่งจะพบว่า Error ซึ่งเกิดขึ้นเพราะเราไม่ได้สร้าง Table ไว้

ซึ่งวิธีแก้นั้นไม่ยากเราสามารถไปสร้าง Table เอง หรือ ให้ตัว spring ทำการสร้างได้โดยไปทำการ Config เพิ่มที่ application.properties

1
spring.jpa.hibernate.ddl-auto=update

โดยตัว config นี้จะทำการ Update ตัวโครงสร้าง table ให้อัตโนมัติแนะนำให้ใช้ช่วง dev อย่างเดียว

จากนั้นลองทำการ Start server อีกครั้งแล้วลองยิง Request จะเห็นว่าไม่ Error แล้ว ซึ่งเมื่อไป check ใน Database ก็จะเห็นว่ามีข้อมูลแล้วดังภาพ

มา Insert เข้าไปจริง แต่เราอยากเห็นคำสั่ง SQL อะ จะได้มั่นใจว่ามันทำงานถูกต้อง ตรงส่วนนี้ก็สามารถ Config ได้ครับ โดยเพิ่ม config ที่ application.properties

1
2
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true

ซึ่งเมื่อลองยิง Request ใหม่จะเห็นว่ามี SQL Query โผล่ขึ้นมาให้เห็นแล้ว ตรงนี้อาจจะงงว่าทำไมมี SELECT ด้วย คือไม่ต้องตกใจครับตัว JPA เขาต้องการเช็คก่อนว่าเคยมี Object นี้อยู่ใน Table ไหม เขาเลยต้องทำการ SELECT ด้วย ID ก่อนเพื่อหาว่ามีไหม ถ้าไม่มีเขาจะสร้าง SQL INSERT แต่ถ้ามีเขาจะสร้าง SQL UPDATE แทน

SELECT ด้วยเงื่อนไขต่างๆ

ตอนนี้เราสามารถเก็บข้อมูลลง Database ได้แล้ว คราวนี้เรามาลอง SELECT ข้อมูลกันดูบ้าง โดยอย่างแรกเพิ่มข้อมูลลง database กันก่อน

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('3aec3c1d-ffa5-4775-8528-80f8b99bc6e5', 'Thorstein Betchley', 36, '9 Springs Road');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('c9c03b28-5e59-4eec-9f3d-70c600c5c9f2', 'Vyky Paulazzi', 82, '6269 Arapahoe Parkway');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('ced90a98-25eb-4c43-9fe3-947a66454117', 'Tobie Orrock', 85, '880 Cambridge Street');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('c1c3c3f9-0e02-47e1-b2d8-485d0179bf51', 'Venita Leatt', 68, '40 Hanover Circle');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('ae6c5150-f999-4c4a-a570-599ea8eaf2ef', 'Teodorico Boraston', 67, '57 Alpine Road');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('d6018263-cf98-413d-9cb9-9d4b9aa1711a', 'Aron Spong', 51, '95968 Butternut Lane');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('42a520b2-ad93-4a5a-bb82-b0a3f4552145', 'Berny Anders', 53, '8 Nelson Park');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('c9f39612-7cda-4e16-9cd8-27809ca9e33f', 'Arny Crosland', 4, '21 Stoughton Terrace');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('48ac94ab-a1c4-40d6-a7d7-6ac01241b443', 'Kevon Glacken', 93, '01562 Briar Crest Junction');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('298dfe35-5fe5-4c1d-bd51-2b3143e46558', 'Dorian Showalter', 45, '22201 Montana Court');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('a06bbc3d-bd73-4531-9c8e-3354b8d79c80', 'Burton Brussell', 68, '33397 1st Place');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('817abbd8-6fd5-4be8-9b86-eb6d7de288b5', 'Reinwald Andrei', 38, '94 Bashford Trail');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('666de36b-ec97-452b-bd6e-2193a16f4dd2', 'Yul Blaxley', 21, '75042 East Plaza');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('6ba0ac17-ac7d-4343-a087-b4abdef0551a', 'Nell Fredson', 77, '53235 Thompson Park');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('cfc126e2-d605-4feb-a879-1c227ceb2d8a', 'Sigismond Bryenton', 64, '212 Orin Avenue');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('13ab7734-66fe-4c72-a8e1-f99d86c361a9', 'Maureen Hansill', 71, '07 Kipling Trail');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('8ecc0507-add3-4842-8198-b29d6890760f', 'Marta Lemonnier', 66, '6157 Carpenter Crossing');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('ccf3cab2-6574-45c0-a3e4-8cc0b9c31b25', 'Wynne Wilden', 59, '23 Thompson Way');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('971a428e-68b4-4f31-81c3-5ce6a7a63775', 'Sallee Stanaway', 77, '02 Menomonie Avenue');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('3dcefbc4-c164-4e17-b141-1e024b01e2ac', 'Cecilla Offa', 81, '56252 Burning Wood Lane');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('ece3f8c6-a5e6-41d7-bdd8-9c1270ac4ede', 'Merv Arnould', 54, '60743 Lillian Terrace');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('ae3ee502-b3eb-48d1-9e61-1562d667c8a6', 'Steffen Imlin', 17, '636 Continental Street');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('5e6fea01-18f1-47f2-9786-8374d14d5638', 'Clemente Ginglell', 73, '4872 Utah Point');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('9a24f317-81c7-42f2-a3d9-287a0476b1ca', 'Peri Putman', 19, '8 Maywood Junction');
insert into people (ID, FULL_NAME, AGE, ADDRESS) values ('a3d59c22-ee42-45c2-997b-551fce33e481', 'Barnebas Matic', 71, '7 Blue Bill Park Plaza');

Build in method

ตัว Spring มี Build in method ที่ใช้ในการค้นหาอยู่แล้วตัวอย่างเช่น findById() , findAll() ตรงนี้สามารถใช้ได้เลยไม่ต้องทำการเขียนเพิ่มเอง

สร้างเงื่อนไขโดยการใช้ชื่อ Method

ถ้า Advance ขึ้นมาหน่อยเราอยากข้อมูลด้วย Field ต่างๆด้วยเงื่อนไขเท่ากับ มากกว่าน้อยกว่า เราก็สามารถสร้างโดยการตั้งชื่อ method โดยมีรายละเอียดดังนี้ Link เช่น ถ้าอยากค้นหาด้วยเงื่อนไขอายุมากกว่า x และชื่อขึ้นต้นด้วย y ก็จะสามารถเขียนชื่อ Method ได้แบบนี้

1
2
3
public interface PeopleRepository extends JpaRepository<People, String> {
List<People> findByAgeGreaterThanAndFullNameStartingWith(Integer age, String fullName);
}

คราวนี้ลองไปทดลองใช้งานที่ Controller

1
2
3
4
@GetMapping("/test-select-with-method-name")
public List<People> testSelectWithMethod() {
return peopleRepository.findByAgeGreaterThanAndFullNameStartingWith(50, "B");
}

ซึ่งเมื่อลองใช้งานแล้วจะได้ผลลัพธ์ดังภาพด้านล่าง

อยากเขียน Query เองไม่อยากสร้างจากชื่อ Method

ในบางกรณีที่ Query มีความซับซ้อนมากๆ หรือ SELECT หลายๆ Field ตัว Spring ก็เปิดโอกาสให้เราเขียน Query นั้นเองโดยใช้ @Query โดย Syntax ที่ใช้ในการเขียนนั้นจะใช้ Syntax JPQL โดยถ้าเราจะใช้เงื่อนไขเดียวกับ Query ก่อนหน้านี้เราสามารถเขียนได้ดังนี้

1
2
3
4
5
6
7
8
9
public interface PeopleRepository extends JpaRepository<People, String> {
List<People> findByAgeGreaterThanAndFullNameStartingWith(Integer age, String fullName);

@Query("SELECT p FROM People p WHERE p.age > :age AND p.fullName like CONCAT(:fullName, '%') ")
List<People> myQuery(
@Param("age") Integer age,
@Param("fullName") String fullName
);
}

คราวนี้ทดลองใช้งานที่ Controller

1
2
3
4
@GetMapping("/test-select-with-query")
public List<People> testSelectWithQuery() {
return peopleRepository.myQuery(50, "B");
}

ซึ่งเมื่อทดลองยิงแล้วจะได้ผลลัพธ์ดังภาพด้านล่าง

ทำ Order และ Pagination ยังไง

คือการดึงข้อมูลนั้นเราคงไม่ดึงข้อมูลทั้งหมดที่ตรงเงื่อนไขกลับไปแน่นอนเพราะมันเปลืองแถมไม่แน่ว่าคน Select เขาไม่ได้สนใจข้อมูลหลังๆด้วย ลองนึกภาพการ Search ของ Google ดูครับ เขาจะแสดงผลการ Search เป็นหน้าๆ เพื่อลดการส่งข้อมูลไปกลับระหว่าง Client กับ Server โดยตัว Spring สามารถทำได้โดย

1
Page<People> findByAgeGreaterThanAndFullNameStartingWith(Integer age, String fullName, Pageable pageable);

จะเห็นว่าเราทำการเพิ่ม parameter อีกตัวคือ Pageable ซึ่งเป็นตัวแปรที่เป็นการบอกว่าจะ Order ด้วยอะไร จะเอาข้อมูล Page ไหน แล้วแต่ละ Page มีขนาดเท่าไหร่ โดยตัวอย่างการใช้งานจะเป็นแบบนี้

1
2
3
4
5
6
@GetMapping("/test-select-with-page")
public List<People> testSelectWithMethodAndPage() {
Pageable pageable = PageRequest.of(0, 2, Sort.by(Sort.Direction.DESC, "age"));
Page<People> result = peopleRepository.findByAgeGreaterThanAndFullNameStartingWith(50, "B", pageable);
return result.getContent();
}

โดยตัวอย่างนั้นเราจะดึงข้อมูล Page แรก (เริ่มที่ 0) และ Page มีขนาด 2 โดยเรียงแบบมากไปน้อยด้วยค่า age ซึ่งจะได้ผลลัพธ์เป็น

สรุป

สำหรับตอนนี้พูดถึงการเชื่อมต่อ Database ว่าทำยังไง ต้องสร้าง Entity ต้องสร้าง Repository ซึ่งด้วยความรู้ประมาณนี้ก็สามารถเขียน Web application server กันได้แล้ว ในส่วนของตอนถัดไปนั้นขอคิดดูก่อนว่าจะเขียนอะไรเพราะทั้ง 4 ตอนก็ครอบคลุมการเขียน Web application server แล้ว ไม่แน่อาจจะไปเขียนเรื่องอื่นเลยก็ได้ สุดท้ายก็ฝากติดตาม Page Normal Programmer ของผมด้วย

เพลงประกอบการเขียน Blog

เพลงเกี่ยวก้อยของวง Wisdom ลองไปฟังดูครับ