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

52
.gitignore vendored Normal file
View File

@@ -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

10
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# 依赖于环境的 Maven 主目录路径
/mavenHomeManager.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

6
.idea/MarsCodeWorkspaceAppSettings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.codeverse.userSettings.MarscodeWorkspaceAppSettingsState">
<option name="progress" value="1.0" />
</component>
</project>

19
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="demo-sqlite-mybatis-hibernate" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="demo-sqlite-mybatis-hibernate" options="-parameters" />
</option>
</component>
</project>

6
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
</component>
</project>

20
.idea/jarRepositories.xml generated Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

12
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="openjdk-21" project-jdk-type="JavaSDK" />
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

4
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive"
}

40
README.md Normal file
View File

@@ -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`

BIN
my-database.db Normal file

Binary file not shown.

82
pom.xml Normal file
View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo-sqlite-mybatis-hibernate</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo-sqlite-mybatis-hibernate</name>
<description>Demo project for Spring Boot with SQLite, MyBatis and Hibernate</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Hibernate Community Dialects for SQLite -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-community-dialects</artifactId>
<version>6.4.1.Final</version>
</dependency>
<dependency>
<groupId>com.github.bohnman</groupId>
<artifactId>squiggly-filter-jackson</artifactId>
<version>1.3.18</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.demo.mapper.BookMapper">
<resultMap id="BookResultMap" type="com.example.demo.entity.Book">
<id property="id" column="id"/>
<result property="title" column="title"/>
<result property="isbn" column="isbn"/>
<association property="author" javaType="com.example.demo.entity.Author">
<id property="id" column="author_id"/>
<result property="name" column="author_name"/>
</association>
<association property="publisher" javaType="com.example.demo.entity.Publisher">
<id property="id" column="publisher_id"/>
<result property="name" column="publisher_name"/>
</association>
</resultMap>
<select id="findByAuthorName" resultMap="BookResultMap">
SELECT b.id, b.title, b.isbn,
a.id as author_id, a.name as author_name,
p.id as publisher_id, p.name as publisher_name
FROM book b
LEFT JOIN author a ON b.author_id = a.id
LEFT JOIN publisher p ON b.publisher_id = p.id
WHERE a.name LIKE '%' || #{authorName} || '%'
</select>
</mapper>

View File

@@ -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.
}
}

View File

@@ -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);
}
}

View File

@@ -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<Book> books = bookMapper.findAll();
assertThat(books).isNotEmpty();
}
@Test
void testFindByAuthorName() {
// DataInitializer inserts "George Orwell"
List<Book> books = bookMapper.findByAuthorName("Orwell");
assertThat(books).isNotEmpty();
assertThat(books.get(0).getAuthor().getName()).contains("Orwell");
}
}

View File

@@ -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<Author> 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<Book> 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");
}
}

BIN
test_debug.log Normal file

Binary file not shown.

BIN
test_output.log Normal file

Binary file not shown.

BIN
test_run.log Normal file

Binary file not shown.