S SmartDocs
Serie: Springboot java 171 líneas · Actualizado 2026-08-14

BookService.java

Springboot/bookstore-api/src/main/java/com/example/bookstore/service/BookService.java

package com.example.bookstore.service;

import com.example.bookstore.dto.BookPatchRequest;
import com.example.bookstore.dto.BookRequest;
import com.example.bookstore.dto.BookResponse;
import com.example.bookstore.dto.PageResponse;
import com.example.bookstore.entity.Book;
import com.example.bookstore.exception.DuplicateResourceException;
import com.example.bookstore.exception.ResourceNotFoundException;
import com.example.bookstore.repository.BookRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * 書籍 Service(商業邏輯層)。
 *
 * 【三層式架構中的角色】
 *   Controller(接 HTTP)→ Service(商業邏輯)→ Repository(存取資料庫)
 *
 * Service 層的職責:
 *   1. 商業規則(例如:ISBN 不可重複)
 *   2. 交易管理(@Transactional)
 *   3. Entity 與 DTO 之間的轉換
 *
 * 【@Transactional 交易管理】
 * 標註後,方法內的所有資料庫操作會包在「同一個交易」裡:
 * 全部成功才 commit;中途拋出例外則全部 rollback,確保資料一致性。
 * readOnly = true 用於純查詢方法,可讓資料庫與 Hibernate 做效能最佳化。
 */
@Service
public class BookService {

    private final BookRepository bookRepository;

    /**
     * 【建構子注入 Constructor Injection】
     * Spring 建立 BookService 時,會自動把 BookRepository 的實例傳進來
     * (這就是「依賴注入 DI」)。相較於 @Autowired 欄位注入,
     * 建構子注入的依賴關係更明確、欄位可宣告為 final、也更容易撰寫單元測試。
     */
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    /** 分頁查詢所有書籍(給 GET /api/books 使用) */
    @Transactional(readOnly = true)
    public PageResponse<BookResponse> findAll(Pageable pageable) {
        // Page.map():把分頁裡的每個 Book Entity 轉成 BookResponse DTO
        Page<BookResponse> page = bookRepository.findAll(pageable)
                .map(BookResponse::from);
        return PageResponse.from(page);
    }

    /** 依 id 查詢單一書籍(給 GET /api/books/{id} 使用) */
    @Transactional(readOnly = true)
    public BookResponse findById(Long id) {
        Book book = getBookOrThrow(id);
        return BookResponse.from(book);
    }

    /** 關鍵字搜尋(給 GET /api/books/search?keyword=xxx 使用) */
    @Transactional(readOnly = true)
    public List<BookResponse> search(String keyword) {
        return bookRepository.searchByKeyword(keyword).stream()
                .map(BookResponse::from)
                .toList();
    }

    /** 新增書籍(給 POST /api/books 使用) */
    @Transactional
    public BookResponse create(BookRequest request) {
        // 商業規則:ISBN 不可重複
        if (bookRepository.existsByIsbn(request.isbn())) {
            throw new DuplicateResourceException("ISBN 已存在:" + request.isbn());
        }

        Book book = new Book(
                request.title(), request.author(), request.isbn(),
                request.price(), request.stock(), request.description());

        // save() 回傳「儲存後」的 Entity(此時 id 已由資料庫產生)
        Book saved = bookRepository.save(book);
        return BookResponse.from(saved);
    }

    /**
     * 完整更新書籍(給 PUT /api/books/{id} 使用)。
     * PUT 的語意是「整個資源替換」:所有欄位一律以請求內容覆寫。
     */
    @Transactional
    public BookResponse update(Long id, BookRequest request) {
        Book book = getBookOrThrow(id);

        // 檢查新 ISBN 是否與「其他書籍」衝突(排除自己)
        if (bookRepository.existsByIsbnAndIdNot(request.isbn(), id)) {
            throw new DuplicateResourceException("ISBN 已被其他書籍使用:" + request.isbn());
        }

        // 覆寫全部欄位
        book.setTitle(request.title());
        book.setAuthor(request.author());
        book.setIsbn(request.isbn());
        book.setPrice(request.price());
        book.setStock(request.stock());
        book.setDescription(request.description());

        // 【JPA 小知識:Dirty Checking 髒值檢查】
        // book 是交易中的「受管理(Managed)」Entity,
        // 交易提交時 Hibernate 會自動偵測欄位變更並送出 UPDATE。
        //
        // 這裡用 saveAndFlush()(而非 save())立刻執行 UPDATE:
        // @PreUpdate 回呼是在「flush」階段才觸發的,
        // 若等交易提交才 flush,回應中的 updatedAt 會是舊值。
        Book saved = bookRepository.saveAndFlush(book);
        return BookResponse.from(saved);
    }

    /**
     * 部分更新書籍(給 PATCH /api/books/{id} 使用)。
     * PATCH 的語意是「只修改有提供的欄位」:null 代表「不要動這個欄位」。
     */
    @Transactional
    public BookResponse patch(Long id, BookPatchRequest request) {
        Book book = getBookOrThrow(id);

        // 逐一判斷:有帶值(非 null)的欄位才更新
        if (request.title() != null) {
            book.setTitle(request.title());
        }
        if (request.author() != null) {
            book.setAuthor(request.author());
        }
        if (request.isbn() != null) {
            if (bookRepository.existsByIsbnAndIdNot(request.isbn(), id)) {
                throw new DuplicateResourceException("ISBN 已被其他書籍使用:" + request.isbn());
            }
            book.setIsbn(request.isbn());
        }
        if (request.price() != null) {
            book.setPrice(request.price());
        }
        if (request.stock() != null) {
            book.setStock(request.stock());
        }
        if (request.description() != null) {
            book.setDescription(request.description());
        }

        // 同 update():立刻 flush,讓 @PreUpdate 更新 updatedAt 後再組回應
        Book saved = bookRepository.saveAndFlush(book);
        return BookResponse.from(saved);
    }

    /** 刪除書籍(給 DELETE /api/books/{id} 使用) */
    @Transactional
    public void delete(Long id) {
        // 先確認資料存在,不存在就回 404(而不是默默地什麼都沒刪)
        Book book = getBookOrThrow(id);
        bookRepository.delete(book);
    }

    /** 共用小工具:依 id 取書,查無資料就拋出 404 例外 */
    private Book getBookOrThrow(Long id) {
        return bookRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("書籍", "id", id));
    }
}

Artículos relacionados

Springboot java EN Actualizado 2026-08-14

BookstoreApiApplication.java

BookstoreApiApplication.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/BookstoreApiApplication.java).

Leer artículo →
Springboot java EN Actualizado 2026-08-14

DataInitializer.java

DataInitializer.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/config/DataInitializer.java).

Leer artículo →
Springboot java EN Actualizado 2026-08-14

SecurityConfig.java

SecurityConfig.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/config/SecurityConfig.java).

Leer artículo →
Springboot java EN Actualizado 2026-08-14

AuthController.java

AuthController.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/controller/AuthController.java).

Leer artículo →
Springboot java EN Actualizado 2026-08-14

BookController.java

BookController.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/controller/BookController.java).

Leer artículo →
Springboot java EN Actualizado 2026-08-14

ApiErrorResponse.java

ApiErrorResponse.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/dto/ApiErrorResponse.java).

Leer artículo →