This commit is contained in:
2026-01-23 15:24:29 +08:00
commit a146fd68a9
40 changed files with 1070 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
package com.example.demo;
import com.example.demo.service.BookService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DataInitializer {
@Bean
public CommandLineRunner initData(BookService bookService) {
return args -> {
bookService.createBook("The Great Gatsby", "1234567890", "F. Scott Fitzgerald", "Scribner");
bookService.createBook("1984", "0987654321", "George Orwell", "Secker & Warburg");
System.out.println("Sample data initialized.");
};
}
}

View File

@@ -0,0 +1,13 @@
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

View File

@@ -0,0 +1,57 @@
package com.example.demo.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.bohnman.squiggly.Squiggly;
import com.github.bohnman.squiggly.context.provider.AbstractSquigglyContextProvider;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
public class SquigglyConfig {
private static final ThreadLocal<String> FILTER_HOLDER = new ThreadLocal<>();
@Bean
public FilterRegistrationBean<SquigglyFilter> squigglyFilter(ObjectMapper objectMapper) {
// Use a custom provider that reads from our ThreadLocal
// This avoids javax.servlet dependency issues in RequestSquigglyContextProvider with Spring Boot 3
Squiggly.init(objectMapper, new AbstractSquigglyContextProvider() {
@Override
public String getFilter(Class beanClass) {
return FILTER_HOLDER.get();
}
@Override
public boolean isFilteringEnabled() {
return FILTER_HOLDER.get() != null;
}
});
FilterRegistrationBean<SquigglyFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new SquigglyFilter());
registration.setOrder(1);
return registration;
}
public static class SquigglyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String fields = request.getParameter("fields");
// Set the filter expression for the current thread
FILTER_HOLDER.set(fields);
try {
filterChain.doFilter(request, response);
} finally {
// Clean up to prevent memory leaks
FILTER_HOLDER.remove();
}
}
}
}

View File

@@ -0,0 +1,47 @@
package com.example.demo.controller;
import com.example.demo.repository.BaseRepository;
import com.example.demo.repository.GenericSpecification;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
public abstract class BaseController<T, ID extends Serializable> {
private final BaseRepository<T, ID> repository;
@Autowired
protected ObjectMapper objectMapper;
public BaseController(BaseRepository<T, ID> repository) {
this.repository = repository;
}
@GetMapping
public Object list(@RequestParam(required = false) Map<String, String> params) {
// 1. Dynamic Search (Hibernate)
List<T> results;
if (params == null || params.isEmpty()) {
results = repository.findAll();
} else {
results = repository.findAll(GenericSpecification.fromMap(params));
}
// 2. Dynamic Projection (Squiggly)
// If "fields" param exists, Squiggly filter is applied automatically by the registered filter.
// But if we want to return a specific structure or verify it, we return the list directly.
// The Squiggly response filter will intercept this return value.
return results;
}
@GetMapping("/{id}")
public T get(@PathVariable ID id) {
return repository.findById(id).orElseThrow(() -> new RuntimeException("Entity not found"));
}
}

View File

@@ -0,0 +1,51 @@
package com.example.demo.controller;
import com.example.demo.entity.Book;
import com.example.demo.service.BookService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/books")
public class BookController extends BaseController<Book, Long> {
private final BookService bookService;
public BookController(BookService bookService, com.example.demo.repository.BookRepository bookRepository) {
super(bookRepository);
this.bookService = bookService;
}
@PostMapping
public Book createBook(@RequestParam String title,
@RequestParam String isbn,
@RequestParam String author,
@RequestParam String publisher) {
return bookService.createBook(title, isbn, author, publisher);
}
// Override the base list method to map it to /jpa specifically, or just use the base one
// Here we expose the generic list capability at /jpa endpoint to match previous behavior
@GetMapping("/jpa")
public Object getAllJPA(@RequestParam(required = false) java.util.Map<String, String> allParams) {
return super.list(allParams);
}
// Original methods for comparison
@GetMapping("/jpa/{id}")
public Book getBookById(@PathVariable Long id) {
return super.get(id);
}
@GetMapping("/mybatis")
public List<Book> getAllMyBatis() {
return bookService.getAllBooksUsingMyBatis();
}
@GetMapping("/search")
public List<Book> searchByAuthor(@RequestParam String author) {
return bookService.searchBooksByAuthorUsingMyBatis(author);
}
}

View File

@@ -0,0 +1,29 @@
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.Data;
import lombok.ToString;
import java.util.List;
@Entity
@Data
@com.fasterxml.jackson.annotation.JsonIdentityInfo(
generator = com.fasterxml.jackson.annotation.ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "region_id")
private Region region;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
@ToString.Exclude
private List<Book> books;
}

View File

@@ -0,0 +1,37 @@
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.Data;
@Entity
@Data
@com.fasterxml.jackson.annotation.JsonIdentityInfo(
generator = com.fasterxml.jackson.annotation.ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String isbn;
@ManyToOne
@JoinColumn(name = "author_id")
private Author author;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "publisher_id")
private Publisher publisher;
@Column(name = "publish_month")
private String publishMonth;
@ManyToOne
@JoinColumns({
@JoinColumn(name = "publisher_id", referencedColumnName = "publisher_id", insertable = false, updatable = false),
@JoinColumn(name = "publish_month", referencedColumnName = "publish_month", insertable = false, updatable = false)
})
private Periodical periodical;
}

View File

@@ -0,0 +1,28 @@
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.Data;
@Entity
@Data
@com.fasterxml.jackson.annotation.JsonIdentityInfo(
generator = com.fasterxml.jackson.annotation.ObjectIdGenerators.PropertyGenerator.class,
property = "id")
@Table(uniqueConstraints = {
@UniqueConstraint(columnNames = {"publisher_id", "publish_month"})
})
public class Periodical {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(name = "publish_month", nullable = false)
private String publishMonth; // Format: YYYY-MM
@ManyToOne(optional = false)
@JoinColumn(name = "publisher_id", nullable = false)
private Publisher publisher;
}

View File

@@ -0,0 +1,30 @@
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.Data;
import lombok.ToString;
import java.util.List;
@Entity
@Data
@com.fasterxml.jackson.annotation.JsonIdentityInfo(
generator = com.fasterxml.jackson.annotation.ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Publisher {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String address;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "region_id")
private Region region;
@OneToMany(mappedBy = "publisher", cascade = CascadeType.ALL)
@ToString.Exclude
private List<Book> books;
}

View File

@@ -0,0 +1,18 @@
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.Data;
@Entity
@Data
@com.fasterxml.jackson.annotation.JsonIdentityInfo(
generator = com.fasterxml.jackson.annotation.ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Region {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}

View File

@@ -0,0 +1,18 @@
package com.example.demo.mapper;
import com.example.demo.entity.Book;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface BookMapper {
// Using Annotation
@Select("SELECT * FROM book")
List<Book> findAll();
// Using XML (defined in resources/mapper/BookMapper.xml)
List<Book> findByAuthorName(String authorName);
}

View File

@@ -0,0 +1,7 @@
package com.example.demo.repository;
import com.example.demo.entity.Author;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AuthorRepository extends JpaRepository<Author, Long> {
}

View File

@@ -0,0 +1,11 @@
package com.example.demo.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.NoRepositoryBean;
import java.io.Serializable;
@NoRepositoryBean
public interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
}

View File

@@ -0,0 +1,7 @@
package com.example.demo.repository;
import com.example.demo.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BookRepository extends BaseRepository<Book, Long> {
}

View File

@@ -0,0 +1,47 @@
package com.example.demo.repository;
import org.springframework.data.jpa.domain.Specification;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class GenericSpecification {
public static <T> Specification<T> fromMap(Map<String, String> filters) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
filters.forEach((key, value) -> {
if (key.equals("fields") || key.equals("sort") || key.equals("page") || key.equals("size")) {
return; // Ignore reserved parameters
}
// Handle nested properties (e.g., "author.name")
Path<String> path = null;
if (key.contains(".")) {
String[] parts = key.split("\\.");
path = root.get(parts[0]);
for (int i = 1; i < parts.length; i++) {
path = path.get(parts[i]);
}
} else {
path = root.get(key);
}
// Improved implementation:
// 1. If value contains "*", replace with "%" and use LIKE
// 2. Otherwise use EQUAL
if (path.getJavaType() == String.class && value.contains("*")) {
String likePattern = value.replace("*", "%");
predicates.add(criteriaBuilder.like(path, likePattern));
} else {
predicates.add(criteriaBuilder.equal(path, value));
}
});
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}
}

View File

@@ -0,0 +1,9 @@
package com.example.demo.repository;
import com.example.demo.entity.Periodical;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PeriodicalRepository extends JpaRepository<Periodical, Long> {
}

View File

@@ -0,0 +1,7 @@
package com.example.demo.repository;
import com.example.demo.entity.Publisher;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PublisherRepository extends JpaRepository<Publisher, Long> {
}

View File

@@ -0,0 +1,9 @@
package com.example.demo.repository;
import com.example.demo.entity.Region;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RegionRepository extends JpaRepository<Region, Long> {
}

View File

@@ -0,0 +1,80 @@
package com.example.demo.service;
import com.example.demo.entity.Author;
import com.example.demo.entity.Book;
import com.example.demo.entity.Periodical;
import com.example.demo.entity.Publisher;
import com.example.demo.entity.Region;
import com.example.demo.mapper.BookMapper;
import com.example.demo.repository.AuthorRepository;
import com.example.demo.repository.BookRepository;
import com.example.demo.repository.PeriodicalRepository;
import com.example.demo.repository.PublisherRepository;
import com.example.demo.repository.RegionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class BookService {
private final BookRepository bookRepository;
private final AuthorRepository authorRepository;
private final PublisherRepository publisherRepository;
private final RegionRepository regionRepository;
private final PeriodicalRepository periodicalRepository;
private final BookMapper bookMapper;
@Transactional
public Book createBook(String title, String isbn, String authorName, String publisherName) {
Region region = new Region();
region.setName("Default Region");
region = regionRepository.save(region);
Author author = new Author();
author.setName(authorName);
author.setRegion(region);
author = authorRepository.save(author);
Publisher publisher = new Publisher();
publisher.setName(publisherName);
publisher.setRegion(region);
publisher = publisherRepository.save(publisher);
// Create a periodical matching this publisher and month
Periodical periodical = new Periodical();
periodical.setName("Monthly Journal " + publisherName);
periodical.setPublisher(publisher);
periodical.setPublishMonth("2023-01");
periodicalRepository.save(periodical);
Book book = new Book();
book.setTitle(title);
book.setIsbn(isbn);
book.setAuthor(author);
book.setPublisher(publisher);
book.setPublishMonth("2023-01"); // This should link to the periodical
book.setPeriodical(periodical);
return bookRepository.save(book);
}
public List<Book> getAllBooksUsingJPA() {
return bookRepository.findAll();
}
public Book getBookById(Long id) {
return bookRepository.findById(id).orElseThrow(() -> new RuntimeException("Book not found"));
}
public List<Book> getAllBooksUsingMyBatis() {
return bookMapper.findAll();
}
public List<Book> searchBooksByAuthorUsingMyBatis(String authorName) {
return bookMapper.findByAuthorName(authorName);
}
}