14 - User CRUD กับฐานข้อมูล
เป้าหมายของบท
Section titled “เป้าหมายของบท”บทนี้จะเปลี่ยน User API จากการใช้ List<User> ใน memory ไปใช้ PostgreSQL จริง
หลังจบบทนี้ผู้อ่านควรมี endpoint:
GET /api/v1/usersGET /api/v1/users/{id}POST /api/v1/usersDELETE /api/v1/users/{id}บทที่ 15 จะเติม PUT /api/v1/users/{id} สำหรับแก้ไขข้อมูล
ภาพรวม flow
Section titled “ภาพรวม flow”เมื่อ client เรียก API flow จะเป็น:
Postman -> UserController -> UserService -> UserRepository -> PostgreSQLController รับ request และส่ง response
Service จัดการ logic เช่น เช็ก email ซ้ำ สร้าง user และหา user
Repository ติดต่อ database
สร้าง UserService
Section titled “สร้าง UserService”สร้างไฟล์:
src/main/java/com/example/backendapi/service/UserService.javapackage com.example.backendapi.service;
import com.example.backendapi.model.User;import com.example.backendapi.repository.UserRepository;import java.util.List;import lombok.RequiredArgsConstructor;import org.springframework.stereotype.Service;
@Service@RequiredArgsConstructorpublic class UserService {
private final UserRepository userRepository;
public List<User> findAll() { return userRepository.findAll(); }
public User findById(Long id) { return userRepository.findById(id) .orElseThrow(() -> new RuntimeException("User not found")); }
public User create(User request) { if (userRepository.existsByUsername(request.getUsername())) { throw new RuntimeException("Username already exists"); }
if (userRepository.existsByEmail(request.getEmail())) { throw new RuntimeException("Email already exists"); }
User user = new User(); user.setUsername(request.getUsername()); user.setEmail(request.getEmail()); user.setPassword(request.getPassword());
return userRepository.save(user); }
public void deleteById(Long id) { User user = findById(id); userRepository.delete(user); }}ในบทนี้ยังใช้ RuntimeException เพื่อให้ flow อ่านง่ายก่อน บท 17 - Exception Handling จะปรับให้ error response สวยและชัดเจนขึ้น
สร้าง UserController
Section titled “สร้าง UserController”สร้างไฟล์:
src/main/java/com/example/backendapi/controller/UserController.javapackage com.example.backendapi.controller;
import com.example.backendapi.model.Role;import com.example.backendapi.model.User;import com.example.backendapi.model.UserStatus;import com.example.backendapi.service.UserService;import java.net.URI;import java.util.List;import lombok.RequiredArgsConstructor;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.DeleteMapping;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;
@RestController@RequestMapping("/api/v1/users")@RequiredArgsConstructorpublic class UserController {
private final UserService userService;
@GetMapping public List<UserResponse> findAll() { return userService.findAll().stream() .map(this::toResponse) .toList(); }
@GetMapping("/{id}") public UserResponse findById(@PathVariable Long id) { return toResponse(userService.findById(id)); }
@PostMapping public ResponseEntity<UserResponse> create(@RequestBody User request) { User user = userService.create(request); URI location = URI.create("/api/v1/users/" + user.getId()); return ResponseEntity.created(location).body(toResponse(user)); }
@DeleteMapping("/{id}") public ResponseEntity<Void> deleteById(@PathVariable Long id) { userService.deleteById(id); return ResponseEntity.noContent().build(); }
private UserResponse toResponse(User user) { return new UserResponse( user.getId(), user.getUsername(), user.getEmail(), user.getRole(), user.getStatus() ); }
private record UserResponse( Long id, String username, String email, Role role, UserStatus status ) { }}จุดสำคัญคือ response ไม่ส่ง password กลับไป
แม้บท DTO จะอยู่ภาคถัดไป แต่เราจะเริ่มแยก response แบบเล็กที่สุดตั้งแต่ตรงนี้ เพราะ password เป็นข้อมูลที่ไม่ควรถูกส่งออกจาก API
ทดสอบสร้าง user
Section titled “ทดสอบสร้าง user”ส่ง request:
POST /api/v1/usersContent-Type: application/json{ "username": "john", "email": "john@example.com", "password": "password123"}response ที่ควรได้:
{ "id": 1, "username": "john", "email": "john@example.com", "role": "USER", "status": "ACTIVE"}สังเกตว่าไม่มี password ใน response
ทดสอบอ่านข้อมูล
Section titled “ทดสอบอ่านข้อมูล”ดึง user ทั้งหมด:
GET /api/v1/usersดึง user ตาม id:
GET /api/v1/users/1ทดสอบลบข้อมูล
Section titled “ทดสอบลบข้อมูล”DELETE /api/v1/users/1ถ้าลบสำเร็จควรได้:
204 No Contentถ้าลองดึง user id เดิมอีกครั้ง ในตอนนี้จะได้ error แบบ default ของ Spring Boot เพราะเรายังไม่ได้ทำ exception handler
ข้อจำกัดของบทนี้
Section titled “ข้อจำกัดของบทนี้”โค้ดในบทนี้ยังไม่ใช่ final production version เพราะ:
- ยังรับ request ด้วย entity ตรง ๆ
- ยังใช้
RuntimeException - ยังไม่ได้ validate request body
- password ยังไม่ได้ hash
- ยังไม่มี pagination
ข้อจำกัดเหล่านี้ตั้งใจเว้นไว้ เพราะจะถูกแก้ทีละเรื่องในภาคถัดไป
แบบฝึกหัดท้ายบท
Section titled “แบบฝึกหัดท้ายบท”- สร้าง user 2 คน
- เรียก
GET /api/v1/users - ลบ user คนแรก
- เรียก
GET /api/v1/usersอีกครั้ง - ตรวจใน PostgreSQL ว่า row ถูกลบจริง