การเพิ่มฐานข้อมูล PostgreSQL ให้กับบริการ RESTful บน Spring Boot ส่วนที่ 1 ในส่วนสุดท้าย เราได้เรียนรู้วิธีการติดตั้งฐานข้อมูล PostgresSQL บนคอมพิวเตอร์ สร้างฐานข้อมูลใน pgAdmin รวมถึงสร้างและลบตารางในฐานข้อมูลด้วยตนเองและทางโปรแกรมด้วย ในส่วนนี้ เราจะเขียนโปรแกรมของเราใหม่เพื่อที่จะเรียนรู้การทำงานกับฐานข้อมูลและตารางนี้ ทำไมต้องเป็นเรา? เพราะตัวฉันเองกำลังเรียนรู้กับคุณจากเนื้อหานี้ จากนั้นเราจะไม่เพียง แต่แก้ไขงานที่มีอยู่เท่านั้น แต่ยังแก้ไขข้อผิดพลาดที่เกิดขึ้นระหว่างเดินทางด้วยความช่วยเหลือจากคำแนะนำจากโปรแกรมเมอร์ที่มีประสบการณ์มากกว่า เราจะเรียนรู้การทำงานเป็นทีม ;) ก่อนอื่น มาสร้าง
com.javarush.lectures.rest_example
แพ็คเกจใหม่ในโฟลเดอร์แล้วเรียกมันว่าrepository
. ในแพ็คเกจนี้เราจะสร้างอินเทอร์เฟซใหม่ClientRepository
:
package com.javarush.lectures.rest_example.repository;
import com.javarush.lectures.rest_example.model.Client;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ClientRepository extends JpaRepository<Client, Integer> {
}
อินเทอร์เฟซนี้จะ "มหัศจรรย์" โต้ตอบกับฐานข้อมูลและตารางของเรา ทำไมต้องมหัศจรรย์? เพราะเราไม่จำเป็นต้องเขียนการนำไปปฏิบัติ และ Spring Framework จะจัดเตรียมให้เรา คุณเพียงแค่ต้องสร้างอินเทอร์เฟซดังกล่าวและคุณสามารถใช้ "เวทย์มนตร์" นี้ได้แล้ว ขั้นตอนต่อไปคือการแก้ไขคลาสClient
ดังนี้:
package com.javarush.lectures.rest_example.model;
import javax.persistence.*;
@Entity
@Table(name = "clients")
public class Client {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "name")
private String name;
@Column(name = "email")
private String email;
@Column(name = "phone")
private String phone;
//… getters and setters
}
สิ่งที่เราทำในชั้นเรียนนี้ก็แค่เพิ่มคำอธิบายประกอบลงไป มาดูกันดีกว่า:
- @Entity - ระบุว่า bean (คลาส) นี้เป็นเอนทิตี
- @Table - ระบุชื่อของตารางที่จะแสดงในเอนทิตีนี้
- @Id - รหัสคอลัมน์ (คีย์หลัก - ค่าที่จะใช้เพื่อให้แน่ใจว่าข้อมูลในตารางปัจจุบันไม่ซ้ำกัน หมายเหตุ: Andrei )
- @Column - ระบุชื่อของคอลัมน์ที่แมปกับคุณสมบัติเอนทิตี
- @GeneratedValue - ระบุว่าคุณสมบัตินี้จะถูกสร้างขึ้นตามกลยุทธ์ที่ระบุ
firstName
เราก็จะตั้งชื่อฟิลด์ในfirst_name
ตาราง คำอธิบายประกอบเหล่านี้สามารถตั้งค่าได้โดยตรงบนฟิลด์และบนตัวรับ แต่ถ้าคุณเลือกวิธีใดวิธีหนึ่งเหล่านี้ ให้พยายามรักษาสไตล์นี้ไว้ตลอดทั้งโปรแกรมของคุณ ฉันใช้วิธีแรกเพียงเพื่อทำให้รายการสั้นลง สามารถดูรายการคำอธิบายประกอบเพิ่มเติมสำหรับการทำงานกับฐานข้อมูลได้ที่นี่ ตอนนี้ไปที่ชั้นเรียนClientServiceImpl
แล้วเขียนใหม่ดังนี้:
package com.javarush.lectures.rest_example.service;
import com.javarush.lectures.rest_example.model.Client;
import com.javarush.lectures.rest_example.repository.ClientRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ClientServiceImpl implements ClientService {
@Autowired
private ClientRepository clientRepository;
@Override
public void create(Client client) {
clientRepository.save(client);
}
@Override
public List<Client> readAll() {
return clientRepository.findAll();
}
@Override
public Client read(int id) {
return clientRepository.getOne(id);
}
@Override
public boolean update(Client client, int id) {
if (clientRepository.existsById(id)) {
client.setId(id);
clientRepository.save(client);
return true;
}
return false;
}
@Override
public boolean delete(int id) {
if (clientRepository.existsById(id)) {
clientRepository.deleteById(id);
return true;
}
return false;
}
}
ดังที่คุณเห็นจากรายการ สิ่งที่เราทำคือลบบรรทัดที่เราไม่ต้องการอีกต่อไป:
// Хранorще клиентов
private static final Map<Integer, Client> CLIENT_REPOSITORY_MAP = new HashMap<>();
// Переменная для генерации ID клиента
private static final AtomicInteger CLIENT_ID_HOLDER = new AtomicInteger();
เราได้ประกาศอินเทอร์เฟซของเราแทนClientRepository
และวาง คำอธิบาย ประกอบ @Autowired ไว้ด้านบน เพื่อให้ Spring จะเพิ่มการพึ่งพานี้ให้กับคลาสของเราโดยอัตโนมัติ นอกจากนี้เรายังมอบหมายงานทั้งหมดให้กับอินเทอร์เฟซนี้หรือเป็นการนำไปปฏิบัติซึ่ง Spring จะเพิ่ม มาดูขั้นตอนสุดท้ายและน่าสนใจที่สุดกันดีกว่า - ทดสอบแอปพลิเคชันของเรา เรามาเปิดโปรแกรม Postman กันดีกว่า (ดูวิธีใช้งานได้ที่นี่ ) และส่งคำขอ GET ไปยังที่อยู่นี้: http://localhost:8080/clients เราได้รับคำตอบนี้:
[
{
"id": 1,
"name": "Vassily Petrov",
"email": "vpetrov@jr.com",
"phone": "+7 (191) 322-22-33)"
},
{
"id": 2,
"name": "Pjotr Vasechkin",
"email": "pvasechkin@jr.com",
"phone": "+7 (191) 223-33-22)"
}
]
เราส่งคำขอ POST:
{
"name" : "Amigo",
"email" : "amigo@jr.com",
"phone" : "+7 (191) 746-43-23"
}
และ... เราพบข้อผิดพลาดแรกในโปรแกรม:
{
"timestamp": "2020-03-06T13:21:12.180+0000",
"status": 500,
"error": "Internal Server Error",
"message": "could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement",
"path": "/clients"
}
เราดูบันทึกและพบข้อผิดพลาดต่อไปนี้:
org.postgresql.util.PSQLException: ОШИБКА: повторяющееся meaning ключа нарушает ограничение уникальности "clients_pkey"
Detail: Ключ "(id)=(1)" уже существует.
เราส่งคำขอ POST เดิมอีกครั้ง ผลลัพธ์ก็เหมือนเดิม แต่ด้วยความแตกต่างนี้: Ключ "(id)=(2)" уже существует.
เราส่งคำขอเดิมเป็นครั้งที่สาม และเราได้รับสถานะ: 201 สร้างแล้ว เราส่งคำขอ GET อีกครั้งและได้รับการตอบกลับ:
[
{
"id": 1,
"name": "Vassily Petrov",
"email": "vpetrov@jr.com",
"phone": "+7 (191) 322-22-33)"
},
{
"id": 2,
"name": "Pjotr Vasechkin",
"email": "pvasechkin@jr.com",
"phone": "+7 (191) 223-33-22)"
},
{
"id": 3,
"name": "Amigo",
"email": "amigo@jr.com",
"phone": "+7 (191) 746-43-23"
}
]
สิ่งนี้ชี้ให้เห็นว่าโปรแกรมของเรากำลังเพิกเฉยต่อความจริงที่ว่าตารางนี้ได้รับการเติมไว้ล่วงหน้าแล้ว และกำลังกำหนด ID อีกครั้งโดยเริ่มจากหนึ่ง แมลงคือช่วงเวลาแห่งการทำงาน อย่าเพิ่งหมดหวัง สิ่งนี้เกิดขึ้นบ่อยครั้ง ดังนั้น ฉันจะขอความช่วยเหลือจากเพื่อนร่วมงานที่มีประสบการณ์มากกว่า: “ถึงเพื่อนร่วมงาน โปรดแนะนำวิธีแก้ไขปัญหานี้ในความคิดเห็นเพื่อให้โปรแกรมทำงานได้ตามปกติ” ความช่วยเหลือใช้เวลาไม่นานก็มาถึง และStas Pasinkovบอกฉันในความคิดเห็นว่าฉันต้องมองไปในทิศทางใด ขอขอบคุณเขาเป็นพิเศษสำหรับสิ่งนี้! แต่ประเด็นก็คือในชั้นเรียนClient
ฉันระบุกลยุทธ์สำหรับคำอธิบายประกอบ@GeneratedValue(strategy = GenerationType.IDENTITY)
สำหรับฟิลด์ ไม่ถูก id
ต้อง กลยุทธ์นี้เหมาะสำหรับ MySQL หากเราทำงานร่วมกับ Oracle หรือ PostrgeSQL เราจำเป็นต้องกำหนดกลยุทธ์ที่แตกต่างออกไป คุณสามารถอ่านเพิ่มเติมเกี่ยวกับกลยุทธ์สำหรับคีย์หลักได้ที่นี่ ฉันเลือกกลยุทธ์ GenerationType.SEQUENCE เพื่อนำไปใช้งาน เราจะต้องเขียนไฟล์ initDB.sql ใหม่เล็กน้อย และแน่นอนว่าต้องใส่คำอธิบายประกอบในช่อง id ของคลาส Client เขียน initDB.sql ใหม่:
CREATE TABLE IF NOT EXISTS clients
(
id INTEGER PRIMARY KEY ,
name VARCHAR(200) NOT NULL ,
email VARCHAR(254) NOT NULL ,
phone VARCHAR(50) NOT NULL
);
CREATE SEQUENCE clients_id_seq START WITH 3 INCREMENT BY 1;
สิ่งที่เปลี่ยนแปลงไป: ประเภทของคอลัมน์ id ของตารางของเรามีการเปลี่ยนแปลง แต่จะมีการเปลี่ยนแปลงเพิ่มเติมในภายหลัง เราได้เพิ่มบรรทัดด้านล่างซึ่งเราสร้างลำดับใหม่ client_id_seq ระบุว่าควรเริ่มต้นด้วยสาม (เนื่องจาก id สุดท้ายในไฟล์ populateDB.sql คือ 2) และระบุว่าการเพิ่มขึ้นควรเกิดขึ้นทีละหนึ่ง กลับไปที่ประเภทคอลัมน์ id กัน ที่นี่เราระบุ INTEGER เพราะถ้าเราปล่อยให้ SERIAL ลำดับจะถูกสร้างขึ้นโดยอัตโนมัติด้วยชื่อเดียวกัน client_id_seq แต่จะเริ่มจากหนึ่ง (ซึ่งนำไปสู่ข้อผิดพลาดของโปรแกรม) อย่างไรก็ตาม หากคุณต้องการลบตาราง คุณจะต้องลบลำดับนี้เพิ่มเติมด้วยตนเองผ่านอินเทอร์เฟซ pgAdmin หรือผ่านไฟล์ .sql โดยใช้คำสั่งต่อไปนี้:
DROP TABLE IF EXISTS clients;
DROP SEQUENCE IF EXISTS clients_id_seq
แต่ถ้าคุณไม่ได้ใช้ไฟล์เช่น populateDB.sql เพื่อเติมข้อมูลในตารางในตอนแรก คุณสามารถใช้ประเภท SERIAL หรือ BIGSERIAL สำหรับคีย์หลักได้ และคุณไม่จำเป็นต้องสร้างลำดับด้วยตนเอง ดังนั้นจึงไม่จำเป็นต้องลบ แยกกัน คุณสามารถอ่านเพิ่มเติมเกี่ยวกับลำดับได้จากเว็บไซต์ของ เอกสารPostgreSQL มาดู คำอธิบายประกอบฟิลด์ id
ของคลาสClient
และจัดรูปแบบดังนี้:
@Id
@Column(name = "id")
@SequenceGenerator(name = "clientsIdSeq", sequenceName = "clients_id_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "clientsIdSeq")
private Integer id;
สิ่งที่เราทำ: เราติดตั้งคำอธิบายประกอบใหม่@SequenceGenerator
เพื่อสร้างตัวสร้างลำดับ กำหนดชื่อclientsIdSeq
ระบุว่านี่คือตัวสร้างสำหรับลำดับclients_id_seq
และเพิ่มแอตทริบิวต์allocationSize = 1
นี่เป็นแอตทริบิวต์ทางเลือก แต่ถ้าเราไม่ทำเช่นนี้ เราจะได้รับข้อผิดพลาดต่อไปนี้เมื่อเรารันโปรแกรม:
org.hibernate.MappingException: The increment size of the [clients_id_seq] sequence is set to [50] in the entity mapping while the associated database sequence increment size is [1]
นี่คือสิ่งที่ผู้ใช้Andrei เขียน เกี่ยวกับสิ่งนี้ในความคิดเห็น: allocationSize มีจุดประสงค์หลักเพื่อลดการเดินทางไปยังฐานข้อมูลในโหมดไฮเบอร์เนตสำหรับ "รหัสใหม่" หากค่า == 1 ให้ไฮเบอร์เนตสำหรับแต่ละเอนทิตีใหม่ ก่อนที่จะบันทึกลงในฐานข้อมูล "รัน" ไปยังฐานข้อมูลสำหรับ id หากค่าเป็น > 1 (เช่น 5) ไฮเบอร์เนตจะติดต่อกับฐานข้อมูลสำหรับรหัส "ใหม่" น้อยกว่า (เช่น 5 ครั้ง) และเมื่อมีการติดต่อ ไฮเบอร์เนตจะขอให้ฐานข้อมูลจองหมายเลขนี้ (ในของเรา กรณี 5) ค่า ข้อผิดพลาดที่คุณอธิบายระบุว่าไฮเบอร์เนตต้องการรับรหัสเริ่มต้น 50 รหัส แต่ในฐานข้อมูลคุณระบุว่าคุณพร้อมที่จะออกรหัสสำหรับเอนทิตีนี้ตามรหัสที่ 1เท่านั้น ผู้ใช้ Nikolya Kudryashovพบข้อบกพร่องอื่น: หากคุณเรียกใช้คำขอจากบทความต้นฉบับ http://localhost:8080/clients/1 ข้อผิดพลาดจะถูกส่งกลับ:
{
"timestamp": "2020-04-02T19:20:16.073+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Type definition error: [simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.javarush.lectures.rest_example.model.Client$HibernateProxy$ZA2m7agZ[\"hibernateLazyInitializer\"])",
"path": "/clients/1"
}
ข้อผิดพลาดนี้เกี่ยวข้องกับการเริ่มต้นแบบ Lazy ของ Hibernate และเพื่อกำจัดข้อผิดพลาดนี้ เราจำเป็นต้องเพิ่มคำอธิบายประกอบเพิ่มเติมให้กับคลาส Client:
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
ทางนี้:
@Entity
@Table(name = "clients")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Client {.....}
ตอนนี้เรามารันโปรแกรมของเรากัน (หลังจากลบตารางไคลเอนต์ออกจากฐานข้อมูลแล้วหากตารางนั้นยังคงอยู่จากครั้งล่าสุด) และใส่เครื่องหมายความคิดเห็น 3 บรรทัดจากไฟล์ application.properties:
#spring.datasource.initialization-mode=ALWAYS
#spring.datasource.schema=classpath*:database/initDB.sql
#spring.datasource.data=classpath*:database/populateDB.sql
คราวที่แล้วเราเม้นแค่บรรทัดสุดท้าย แต่... เนื่องจากเราได้สร้างและกรอกตารางแล้ว สิ่งนี้จึงดูสมเหตุสมผลสำหรับฉันมากขึ้นในขณะนี้ มาดูการทดสอบ ดำเนินการคำขอ GET, POST, PUT และ DELETE ผ่านทางบุรุษไปรษณีย์กัน แล้วเราจะเห็นว่าจุดบกพร่องหายไปแล้วและทุกอย่างทำงานได้ดี แค่นั้นแหละ งานเสร็จแล้ว ตอนนี้เราสามารถสรุปสั้นๆ และพิจารณาสิ่งที่เราได้เรียนรู้:
- ติดตั้ง PostgreSQL บนคอมพิวเตอร์ของคุณ
- สร้างฐานข้อมูลใน pgAdmin
- สร้างและลบตารางด้วยตนเองและทางโปรแกรม
- เติมข้อมูลตารางด้วยไฟล์ .sql
- เราได้เรียนรู้เล็กน้อยเกี่ยวกับอินเทอร์เฟซ JpaRepository “วิเศษ” ของ Spring Framework
- เราได้เรียนรู้เกี่ยวกับข้อบกพร่องบางอย่างที่อาจเกิดขึ้นเมื่อสร้างโปรแกรมดังกล่าว
- เราตระหนักว่าเราไม่ควรอายที่จะขอคำแนะนำจากเพื่อนร่วมงาน
- เราได้รับการยืนยันแล้วว่าชุมชน JavaRush เป็นพลังที่จะช่วยเหลือเสมอ ;)
com.javarush.lectures.rest_example
เป็นcom.javarush.lectures.rest.example
และชื่อของโปรเจ็กต์ เพื่อไม่ให้ละเมิดหลักการตั้งชื่อใน Java ผู้ใช้ UPD2 Alexander Pyanovแนะนำว่าการเริ่มต้นฟิลด์ClientRepository
ในคลาสClientServiceImpl
จะดีกว่าถ้าใช้ Constructor มากกว่าคำอธิบาย@Autowired
ประกอบ สิ่งนี้อธิบายได้จากข้อเท็จจริงที่ว่า ในบางกรณีที่เกิดขึ้นไม่บ่อยนัก คุณจะได้รับNullPointerException
และโดยทั่วไป นี่เป็นแนวปฏิบัติที่ดีที่สุด และฉันเห็นด้วยกับมัน ตามตรรกะ หากจำเป็นต้องมีฟิลด์สำหรับฟังก์ชันเริ่มต้นของอ็อบเจ็กต์ ก็ควรเริ่มต้นในคอนสตรัคเตอร์จะดีกว่า เพราะคลาสที่ไม่มีคอนสตรัคเตอร์จะไม่ถูกรวมเข้ากับอ็อบเจ็กต์ ดังนั้น ฟิลด์นี้จะถูกเตรียมใช้งานในสเตจ ของการสร้างวัตถุ ฉันจะเพิ่มส่วนของโค้ดพร้อมการแก้ไข (สิ่งที่ต้องแทนที่ด้วยอะไร):
@Autowired
private ClientRepository clientRepository;
private final ClientRepository clientRepository;
public ClientServiceImpl(ClientRepository clientRepository) {
this.clientRepository = clientRepository;
}
ลิงก์ไปยังส่วนแรก: การเพิ่มฐานข้อมูล PostgreSQL ให้กับบริการ RESTful บน Spring Boot ส่วนที่ 1 ป.ล. หากคุณต้องการพัฒนาแอปพลิเคชันเพื่อการศึกษานี้ต่อไป ฉันยินดีที่จะเพิ่มลิงก์ไปยังคำแนะนำของคุณในบทความนี้ บางทีสักวันหนึ่งโปรแกรมนี้อาจเติบโตเป็นสิ่งที่คล้ายกับแอปพลิเคชันทางธุรกิจจริง ๆ ที่คุณสามารถเพิ่มงานลงในพอร์ตโฟลิโอของคุณได้ PPS เกี่ยวกับบทความที่เรียบง่ายนี้ ฉันตัดสินใจอุทิศการทดสอบปากกานี้ให้กับเด็กผู้หญิง ผู้หญิง และสุภาพสตรีที่รักของเรา ใครจะรู้ บางทีตอนนี้อาจจะไม่มี Java ไม่มี JavaRush ไม่มีการเขียนโปรแกรมโดยธรรมชาติ ถ้าไม่ใช่สำหรับผู้หญิงคน นี้ ขอแสดงความยินดีในวันหยุดของคุณคนฉลาดที่รักของเรา! สุขสันต์วันที่ 8 มีนาคม! มีความสุขและสวยงาม!
GO TO FULL VERSION