commit a146fd68a96afab1d73fd1ea1851a1fe388fb6e4 Author: 吴方圳 <1040079213@qq.com> Date: Fri Jan 23 15:24:29 2026 +0800 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da948b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +###################################################################### +# Build Tools + +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +target/ +!.mvn/wrapper/maven-wrapper.jar + +###################################################################### +# IDE + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### JRebel ### +rebel.xml + +### NetBeans ### +nbproject/private/ +build/* +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +###################################################################### +# Others +*.log +*.xml.versionsBackup +*.swp + +!*/build/*.java +!*/build/*.html +!*/build/*.xml +application-druid.yml +application-pro.yml +#忽略所有target目录 +target/ +.vscode diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..7d05e99 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# 依赖于环境的 Maven 主目录路径 +/mavenHomeManager.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/MarsCodeWorkspaceAppSettings.xml b/.idea/MarsCodeWorkspaceAppSettings.xml new file mode 100644 index 0000000..e2a065b --- /dev/null +++ b/.idea/MarsCodeWorkspaceAppSettings.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..5e51f5f --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..63e9001 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..aacacf5 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e012065 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "java.compile.nullAnalysis.mode": "automatic", + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b41d518 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +## 运行指南 + +这是一个使用 Spring Boot, SQLite, Hibernate (JPA) 和 MyBatis 的示例项目。 + +### 1. 启动应用 + +在项目根目录下运行: + +```bash +mvn spring-boot:run +``` +或者打包运行: +```bash +mvn clean package -DskipTests +java -jar target/demo-sqlite-mybatis-hibernate-0.0.1-SNAPSHOT.jar +``` +*注意:端口配置在 8081* + +### 2. 测试接口 + +项目启动时会自动插入一些测试数据。你可以使用以下接口进行测试: + +- **获取所有书籍 (使用 JPA):** + `GET http://localhost:8081/books/jpa` + +- **获取所有书籍 (使用 MyBatis - 简单映射):** + `GET http://localhost:8081/books/mybatis` + +- **按作者搜索 (使用 MyBatis - 复杂 XML 映射):** + `GET http://localhost:8081/books/search?author=Orwell` + +### 3. 项目结构 + +- **实体类**: `src/main/java/com/example/demo/entity` (Book, Author, Publisher) +- **JPA Repository**: `src/main/java/com/example/demo/repository` +- **MyBatis Mapper**: + - 接口: `src/main/java/com/example/demo/mapper` + - XML: `src/main/resources/mapper` +- **Service**: `src/main/java/com/example/demo/service` +- **Controller**: `src/main/java/com/example/demo/controller` diff --git a/my-database.db b/my-database.db new file mode 100644 index 0000000..68f5c1d Binary files /dev/null and b/my-database.db differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..fa49e9d --- /dev/null +++ b/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.1 + + + com.example + demo-sqlite-mybatis-hibernate + 0.0.1-SNAPSHOT + demo-sqlite-mybatis-hibernate + Demo project for Spring Boot with SQLite, MyBatis and Hibernate + + 17 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 3.0.3 + + + + org.xerial + sqlite-jdbc + runtime + + + + + org.hibernate.orm + hibernate-community-dialects + 6.4.1.Final + + + + com.github.bohnman + squiggly-filter-jackson + 1.3.18 + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/src/main/java/com/example/demo/DataInitializer.java b/src/main/java/com/example/demo/DataInitializer.java new file mode 100644 index 0000000..6598b2c --- /dev/null +++ b/src/main/java/com/example/demo/DataInitializer.java @@ -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."); + }; + } +} diff --git a/src/main/java/com/example/demo/DemoApplication.java b/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 0000000..094d95b --- /dev/null +++ b/src/main/java/com/example/demo/DemoApplication.java @@ -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); + } + +} diff --git a/src/main/java/com/example/demo/config/SquigglyConfig.java b/src/main/java/com/example/demo/config/SquigglyConfig.java new file mode 100644 index 0000000..b97786a --- /dev/null +++ b/src/main/java/com/example/demo/config/SquigglyConfig.java @@ -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 FILTER_HOLDER = new ThreadLocal<>(); + + @Bean + public FilterRegistrationBean 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 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(); + } + } + } +} diff --git a/src/main/java/com/example/demo/controller/BaseController.java b/src/main/java/com/example/demo/controller/BaseController.java new file mode 100644 index 0000000..9076d14 --- /dev/null +++ b/src/main/java/com/example/demo/controller/BaseController.java @@ -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 { + + private final BaseRepository repository; + + @Autowired + protected ObjectMapper objectMapper; + + public BaseController(BaseRepository repository) { + this.repository = repository; + } + + @GetMapping + public Object list(@RequestParam(required = false) Map params) { + // 1. Dynamic Search (Hibernate) + List 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")); + } +} diff --git a/src/main/java/com/example/demo/controller/BookController.java b/src/main/java/com/example/demo/controller/BookController.java new file mode 100644 index 0000000..e9cca5a --- /dev/null +++ b/src/main/java/com/example/demo/controller/BookController.java @@ -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 { + + 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 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 getAllMyBatis() { + return bookService.getAllBooksUsingMyBatis(); + } + + @GetMapping("/search") + public List searchByAuthor(@RequestParam String author) { + return bookService.searchBooksByAuthorUsingMyBatis(author); + } +} diff --git a/src/main/java/com/example/demo/entity/Author.java b/src/main/java/com/example/demo/entity/Author.java new file mode 100644 index 0000000..89aaa83 --- /dev/null +++ b/src/main/java/com/example/demo/entity/Author.java @@ -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 books; +} diff --git a/src/main/java/com/example/demo/entity/Book.java b/src/main/java/com/example/demo/entity/Book.java new file mode 100644 index 0000000..e533685 --- /dev/null +++ b/src/main/java/com/example/demo/entity/Book.java @@ -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; +} diff --git a/src/main/java/com/example/demo/entity/Periodical.java b/src/main/java/com/example/demo/entity/Periodical.java new file mode 100644 index 0000000..89f986a --- /dev/null +++ b/src/main/java/com/example/demo/entity/Periodical.java @@ -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; +} diff --git a/src/main/java/com/example/demo/entity/Publisher.java b/src/main/java/com/example/demo/entity/Publisher.java new file mode 100644 index 0000000..35257c4 --- /dev/null +++ b/src/main/java/com/example/demo/entity/Publisher.java @@ -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 books; +} diff --git a/src/main/java/com/example/demo/entity/Region.java b/src/main/java/com/example/demo/entity/Region.java new file mode 100644 index 0000000..2b354af --- /dev/null +++ b/src/main/java/com/example/demo/entity/Region.java @@ -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; +} diff --git a/src/main/java/com/example/demo/mapper/BookMapper.java b/src/main/java/com/example/demo/mapper/BookMapper.java new file mode 100644 index 0000000..2fe8817 --- /dev/null +++ b/src/main/java/com/example/demo/mapper/BookMapper.java @@ -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 findAll(); + + // Using XML (defined in resources/mapper/BookMapper.xml) + List findByAuthorName(String authorName); +} diff --git a/src/main/java/com/example/demo/repository/AuthorRepository.java b/src/main/java/com/example/demo/repository/AuthorRepository.java new file mode 100644 index 0000000..505c2ea --- /dev/null +++ b/src/main/java/com/example/demo/repository/AuthorRepository.java @@ -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 { +} diff --git a/src/main/java/com/example/demo/repository/BaseRepository.java b/src/main/java/com/example/demo/repository/BaseRepository.java new file mode 100644 index 0000000..3336e09 --- /dev/null +++ b/src/main/java/com/example/demo/repository/BaseRepository.java @@ -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 extends JpaRepository, JpaSpecificationExecutor { +} diff --git a/src/main/java/com/example/demo/repository/BookRepository.java b/src/main/java/com/example/demo/repository/BookRepository.java new file mode 100644 index 0000000..9fae43d --- /dev/null +++ b/src/main/java/com/example/demo/repository/BookRepository.java @@ -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 { +} diff --git a/src/main/java/com/example/demo/repository/GenericSpecification.java b/src/main/java/com/example/demo/repository/GenericSpecification.java new file mode 100644 index 0000000..2851c15 --- /dev/null +++ b/src/main/java/com/example/demo/repository/GenericSpecification.java @@ -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 Specification fromMap(Map filters) { + return (root, query, criteriaBuilder) -> { + List 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 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])); + }; + } +} diff --git a/src/main/java/com/example/demo/repository/PeriodicalRepository.java b/src/main/java/com/example/demo/repository/PeriodicalRepository.java new file mode 100644 index 0000000..afcb75d --- /dev/null +++ b/src/main/java/com/example/demo/repository/PeriodicalRepository.java @@ -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 { +} diff --git a/src/main/java/com/example/demo/repository/PublisherRepository.java b/src/main/java/com/example/demo/repository/PublisherRepository.java new file mode 100644 index 0000000..db5238a --- /dev/null +++ b/src/main/java/com/example/demo/repository/PublisherRepository.java @@ -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 { +} diff --git a/src/main/java/com/example/demo/repository/RegionRepository.java b/src/main/java/com/example/demo/repository/RegionRepository.java new file mode 100644 index 0000000..4857aee --- /dev/null +++ b/src/main/java/com/example/demo/repository/RegionRepository.java @@ -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 { +} diff --git a/src/main/java/com/example/demo/service/BookService.java b/src/main/java/com/example/demo/service/BookService.java new file mode 100644 index 0000000..7f30809 --- /dev/null +++ b/src/main/java/com/example/demo/service/BookService.java @@ -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 getAllBooksUsingJPA() { + return bookRepository.findAll(); + } + + public Book getBookById(Long id) { + return bookRepository.findById(id).orElseThrow(() -> new RuntimeException("Book not found")); + } + + public List getAllBooksUsingMyBatis() { + return bookMapper.findAll(); + } + + public List searchBooksByAuthorUsingMyBatis(String authorName) { + return bookMapper.findByAuthorName(authorName); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..b77e403 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,19 @@ +spring.application.name=demo-sqlite-mybatis-hibernate + +# DataSource +spring.datasource.url=jdbc:sqlite:my-database.db +spring.datasource.driver-class-name=org.sqlite.JDBC +spring.datasource.username= +spring.datasource.password= + +# JPA / Hibernate +spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true + +# MyBatis +mybatis.mapper-locations=classpath:mapper/*.xml +mybatis.type-aliases-package=com.example.demo.entity + +# Server Port +server.port=8081 diff --git a/src/main/resources/mapper/BookMapper.xml b/src/main/resources/mapper/BookMapper.xml new file mode 100644 index 0000000..4667594 --- /dev/null +++ b/src/main/resources/mapper/BookMapper.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/com/example/demo/BaseTest.java b/src/test/java/com/example/demo/BaseTest.java new file mode 100644 index 0000000..d9bcb12 --- /dev/null +++ b/src/test/java/com/example/demo/BaseTest.java @@ -0,0 +1,31 @@ +package com.example.demo; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import com.example.demo.repository.AuthorRepository; +import com.example.demo.repository.BookRepository; +import com.example.demo.repository.PublisherRepository; + +@SpringBootTest +@Transactional // Rollback after each test +public abstract class BaseTest { + + @Autowired + protected BookRepository bookRepository; + @Autowired + protected AuthorRepository authorRepository; + @Autowired + protected PublisherRepository publisherRepository; + + @BeforeEach + void setUp() { + // You can add common setup here if needed + // Note: DataInitializer runs on startup, so data might already be there. + // Since we use @Transactional, changes in tests won't persist, + // but data from DataInitializer will be visible unless we clean it up. + } +} diff --git a/src/test/java/com/example/demo/controller/BookControllerTest.java b/src/test/java/com/example/demo/controller/BookControllerTest.java new file mode 100644 index 0000000..95b7041 --- /dev/null +++ b/src/test/java/com/example/demo/controller/BookControllerTest.java @@ -0,0 +1,130 @@ +package com.example.demo.controller; + +import com.example.demo.BaseTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@AutoConfigureMockMvc +public class BookControllerTest extends BaseTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void testCreateAndGetBook() throws Exception { + mockMvc.perform(post("/books") + .param("title", "Integration Test Book") + .param("isbn", "999-888") + .param("author", "Integration Author") + .param("publisher", "Integration Pub")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.title").value("Integration Test Book")); + + mockMvc.perform(get("/books/jpa")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[?(@.title == 'Integration Test Book')]").exists()); + } + + @Test + void testDynamicSearchAndFilter() throws Exception { + // 1. Create two books with different authors + mockMvc.perform(post("/books") + .param("title", "Java Guide") + .param("isbn", "JG-001") + .param("author", "John Doe") + .param("publisher", "Tech Pub")) + .andExpect(status().isOk()); + + mockMvc.perform(post("/books") + .param("title", "Python Guide") + .param("isbn", "PG-001") + .param("author", "Jane Smith") + .param("publisher", "Tech Pub")) + .andExpect(status().isOk()); + + // 2. Search by author name (partial match with wildcard) and request specific fields + // Request: /books/jpa?author.name=John*&fields=title,author.name + String responseContent = mockMvc.perform(get("/books/jpa") + .param("author.name", "John*") + .param("fields", "title,author.name")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + System.out.println("Dynamic Search Response: " + responseContent); + + // 3. Verify + // Should contain "Java Guide" + // Should NOT contain "Python Guide" + // Should contain "author" object with "name" + // Should NOT contain "isbn" + + // Check if list contains Java Guide + mockMvc.perform(get("/books/jpa") + .param("author.name", "John*") + .param("fields", "title,author.name")) + .andExpect(jsonPath("$[0].title").value("Java Guide")) + .andExpect(jsonPath("$[0].author.name").value("John Doe")) + .andExpect(jsonPath("$[0].isbn").doesNotExist()); + + // Ensure only 1 result (or at least filtered) + // Note: previous tests might have added books, so we just check existence of the match and absence of the mismatch + } + @Test + void testSquigglyFilter() throws Exception { + // 1. Create a book to get an ID + String createResponse = mockMvc.perform(post("/books") + .param("title", "Squiggly Test Book") + .param("isbn", "SQ-123") + .param("author", "Squiggly Author") + .param("publisher", "Squiggly Pub")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + Integer id = com.jayway.jsonpath.JsonPath.read(createResponse, "$.id"); + + // 2. Test dynamic field selection on single record + String responseContent = mockMvc.perform(get("/books/jpa/" + id) + .param("fields", "title,isbn")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + System.out.println("Squiggly Filter Response (Single Record): " + responseContent); + + // 3. Verify + mockMvc.perform(get("/books/jpa/" + id) + .param("fields", "title,isbn")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.title").exists()) + .andExpect(jsonPath("$.isbn").exists()) + .andExpect(jsonPath("$.author").doesNotExist()); + + // 4. Verify deep filtering with Region + String deepResponse = mockMvc.perform(get("/books/jpa/" + id) + .param("fields", "title,author.name,author.region.name")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.title").exists()) + .andExpect(jsonPath("$.author.name").exists()) + .andExpect(jsonPath("$.author.region.name").value("Default Region")) + .andExpect(jsonPath("$.author.region.id").doesNotExist()) + .andReturn().getResponse().getContentAsString(); + + // 5. Verify non-ID association with Periodical + String periodicalResponse = mockMvc.perform(get("/books/jpa/" + id) + .param("fields", "title,publishMonth,periodical.name,periodical.publishMonth")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.title").exists()) + .andExpect(jsonPath("$.publishMonth").value("2023-01")) + .andExpect(jsonPath("$.periodical.name").value("Monthly Journal Squiggly Pub")) + .andExpect(jsonPath("$.periodical.publishMonth").value("2023-01")) + .andReturn().getResponse().getContentAsString(); + + System.out.println("Squiggly Filter Response (Periodical): " + periodicalResponse); + } +} diff --git a/src/test/java/com/example/demo/mapper/BookMapperTest.java b/src/test/java/com/example/demo/mapper/BookMapperTest.java new file mode 100644 index 0000000..3e4ec45 --- /dev/null +++ b/src/test/java/com/example/demo/mapper/BookMapperTest.java @@ -0,0 +1,31 @@ +package com.example.demo.mapper; + +import com.example.demo.BaseTest; +import com.example.demo.entity.Book; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BookMapperTest extends BaseTest { + + @Autowired + private BookMapper bookMapper; + + @Test + void testFindAll() { + // DataInitializer inserts some data + List books = bookMapper.findAll(); + assertThat(books).isNotEmpty(); + } + + @Test + void testFindByAuthorName() { + // DataInitializer inserts "George Orwell" + List books = bookMapper.findByAuthorName("Orwell"); + assertThat(books).isNotEmpty(); + assertThat(books.get(0).getAuthor().getName()).contains("Orwell"); + } +} diff --git a/src/test/java/com/example/demo/repository/AuthorRepositoryTest.java b/src/test/java/com/example/demo/repository/AuthorRepositoryTest.java new file mode 100644 index 0000000..6799a6f --- /dev/null +++ b/src/test/java/com/example/demo/repository/AuthorRepositoryTest.java @@ -0,0 +1,50 @@ +package com.example.demo.repository; + +import com.example.demo.BaseTest; +import com.example.demo.entity.Author; +import com.example.demo.entity.Book; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AuthorRepositoryTest extends BaseTest { + + @Test + void testSaveAndFind() { + Author author = new Author(); + author.setName("Test Author"); + authorRepository.save(author); + + List authors = authorRepository.findAll(); + assertThat(authors).extracting(Author::getName).contains("Test Author"); + } + + @Test + void testAuthorBookRelationship() { + Author author = new Author(); + author.setName("Relation Author"); + + Book book = new Book(); + book.setTitle("Test Book"); + book.setIsbn("111-222"); + book.setAuthor(author); + + // Because CascadeType.ALL is set on Author.books, we should add book to author's list + // BUT currently Book owns the relationship (@ManyToOne). + // Typically we save Author first, then Book. + // Or if we want cascade save from Author, we need to set the list. + + // Let's do standard JPA way + author = authorRepository.save(author); + + book.setAuthor(author); + bookRepository.save(book); + + List books = bookRepository.findAll(); + assertThat(books).extracting(Book::getTitle).contains("Test Book"); + assertThat(books.stream().filter(b -> b.getTitle().equals("Test Book")).findFirst().get().getAuthor().getName()) + .isEqualTo("Relation Author"); + } +} diff --git a/test_debug.log b/test_debug.log new file mode 100644 index 0000000..74a09da Binary files /dev/null and b/test_debug.log differ diff --git a/test_output.log b/test_output.log new file mode 100644 index 0000000..520c4cf Binary files /dev/null and b/test_output.log differ diff --git a/test_run.log b/test_run.log new file mode 100644 index 0000000..520c4cf Binary files /dev/null and b/test_run.log differ