1
This commit is contained in:
19
src/main/java/com/example/demo/DataInitializer.java
Normal file
19
src/main/java/com/example/demo/DataInitializer.java
Normal 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.");
|
||||
};
|
||||
}
|
||||
}
|
||||
13
src/main/java/com/example/demo/DemoApplication.java
Normal file
13
src/main/java/com/example/demo/DemoApplication.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
57
src/main/java/com/example/demo/config/SquigglyConfig.java
Normal file
57
src/main/java/com/example/demo/config/SquigglyConfig.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
29
src/main/java/com/example/demo/entity/Author.java
Normal file
29
src/main/java/com/example/demo/entity/Author.java
Normal 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;
|
||||
}
|
||||
37
src/main/java/com/example/demo/entity/Book.java
Normal file
37
src/main/java/com/example/demo/entity/Book.java
Normal 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;
|
||||
}
|
||||
28
src/main/java/com/example/demo/entity/Periodical.java
Normal file
28
src/main/java/com/example/demo/entity/Periodical.java
Normal 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;
|
||||
}
|
||||
30
src/main/java/com/example/demo/entity/Publisher.java
Normal file
30
src/main/java/com/example/demo/entity/Publisher.java
Normal 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;
|
||||
}
|
||||
18
src/main/java/com/example/demo/entity/Region.java
Normal file
18
src/main/java/com/example/demo/entity/Region.java
Normal 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;
|
||||
}
|
||||
18
src/main/java/com/example/demo/mapper/BookMapper.java
Normal file
18
src/main/java/com/example/demo/mapper/BookMapper.java
Normal 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);
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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]));
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
80
src/main/java/com/example/demo/service/BookService.java
Normal file
80
src/main/java/com/example/demo/service/BookService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user