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