1
This commit is contained in:
52
.gitignore
vendored
Normal file
52
.gitignore
vendored
Normal 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
10
.idea/.gitignore
generated
vendored
Normal 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
6
.idea/MarsCodeWorkspaceAppSettings.xml
generated
Normal 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
19
.idea/compiler.xml
generated
Normal 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
6
.idea/encodings.xml
generated
Normal 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
20
.idea/jarRepositories.xml
generated
Normal 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
12
.idea/misc.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"java.compile.nullAnalysis.mode": "automatic",
|
||||||
|
"java.configuration.updateBuildConfiguration": "interactive"
|
||||||
|
}
|
||||||
40
README.md
Normal file
40
README.md
Normal 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
BIN
my-database.db
Normal file
Binary file not shown.
82
pom.xml
Normal file
82
pom.xml
Normal 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>
|
||||||
19
src/main/java/com/example/demo/DataInitializer.java
Normal file
19
src/main/java/com/example/demo/DataInitializer.java
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package com.example.demo;
|
||||||
|
|
||||||
|
import com.example.demo.service.BookService;
|
||||||
|
import org.springframework.boot.CommandLineRunner;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class DataInitializer {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CommandLineRunner initData(BookService bookService) {
|
||||||
|
return args -> {
|
||||||
|
bookService.createBook("The Great Gatsby", "1234567890", "F. Scott Fitzgerald", "Scribner");
|
||||||
|
bookService.createBook("1984", "0987654321", "George Orwell", "Secker & Warburg");
|
||||||
|
System.out.println("Sample data initialized.");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/main/java/com/example/demo/DemoApplication.java
Normal file
13
src/main/java/com/example/demo/DemoApplication.java
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package com.example.demo;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class DemoApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(DemoApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
57
src/main/java/com/example/demo/config/SquigglyConfig.java
Normal file
57
src/main/java/com/example/demo/config/SquigglyConfig.java
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package com.example.demo.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.github.bohnman.squiggly.Squiggly;
|
||||||
|
import com.github.bohnman.squiggly.context.provider.AbstractSquigglyContextProvider;
|
||||||
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class SquigglyConfig {
|
||||||
|
|
||||||
|
private static final ThreadLocal<String> FILTER_HOLDER = new ThreadLocal<>();
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public FilterRegistrationBean<SquigglyFilter> squigglyFilter(ObjectMapper objectMapper) {
|
||||||
|
// Use a custom provider that reads from our ThreadLocal
|
||||||
|
// This avoids javax.servlet dependency issues in RequestSquigglyContextProvider with Spring Boot 3
|
||||||
|
Squiggly.init(objectMapper, new AbstractSquigglyContextProvider() {
|
||||||
|
@Override
|
||||||
|
public String getFilter(Class beanClass) {
|
||||||
|
return FILTER_HOLDER.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isFilteringEnabled() {
|
||||||
|
return FILTER_HOLDER.get() != null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
FilterRegistrationBean<SquigglyFilter> registration = new FilterRegistrationBean<>();
|
||||||
|
registration.setFilter(new SquigglyFilter());
|
||||||
|
registration.setOrder(1);
|
||||||
|
return registration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SquigglyFilter extends OncePerRequestFilter {
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
String fields = request.getParameter("fields");
|
||||||
|
// Set the filter expression for the current thread
|
||||||
|
FILTER_HOLDER.set(fields);
|
||||||
|
try {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
} finally {
|
||||||
|
// Clean up to prevent memory leaks
|
||||||
|
FILTER_HOLDER.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import com.example.demo.repository.BaseRepository;
|
||||||
|
import com.example.demo.repository.GenericSpecification;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public abstract class BaseController<T, ID extends Serializable> {
|
||||||
|
|
||||||
|
private final BaseRepository<T, ID> repository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
protected ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public BaseController(BaseRepository<T, ID> repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public Object list(@RequestParam(required = false) Map<String, String> params) {
|
||||||
|
// 1. Dynamic Search (Hibernate)
|
||||||
|
List<T> results;
|
||||||
|
if (params == null || params.isEmpty()) {
|
||||||
|
results = repository.findAll();
|
||||||
|
} else {
|
||||||
|
results = repository.findAll(GenericSpecification.fromMap(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Dynamic Projection (Squiggly)
|
||||||
|
// If "fields" param exists, Squiggly filter is applied automatically by the registered filter.
|
||||||
|
// But if we want to return a specific structure or verify it, we return the list directly.
|
||||||
|
// The Squiggly response filter will intercept this return value.
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public T get(@PathVariable ID id) {
|
||||||
|
return repository.findById(id).orElseThrow(() -> new RuntimeException("Entity not found"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.example.demo.controller;
|
||||||
|
|
||||||
|
import com.example.demo.entity.Book;
|
||||||
|
import com.example.demo.service.BookService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/books")
|
||||||
|
public class BookController extends BaseController<Book, Long> {
|
||||||
|
|
||||||
|
private final BookService bookService;
|
||||||
|
|
||||||
|
public BookController(BookService bookService, com.example.demo.repository.BookRepository bookRepository) {
|
||||||
|
super(bookRepository);
|
||||||
|
this.bookService = bookService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public Book createBook(@RequestParam String title,
|
||||||
|
@RequestParam String isbn,
|
||||||
|
@RequestParam String author,
|
||||||
|
@RequestParam String publisher) {
|
||||||
|
return bookService.createBook(title, isbn, author, publisher);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override the base list method to map it to /jpa specifically, or just use the base one
|
||||||
|
// Here we expose the generic list capability at /jpa endpoint to match previous behavior
|
||||||
|
@GetMapping("/jpa")
|
||||||
|
public Object getAllJPA(@RequestParam(required = false) java.util.Map<String, String> allParams) {
|
||||||
|
return super.list(allParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original methods for comparison
|
||||||
|
@GetMapping("/jpa/{id}")
|
||||||
|
public Book getBookById(@PathVariable Long id) {
|
||||||
|
return super.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/mybatis")
|
||||||
|
public List<Book> getAllMyBatis() {
|
||||||
|
return bookService.getAllBooksUsingMyBatis();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
public List<Book> searchByAuthor(@RequestParam String author) {
|
||||||
|
return bookService.searchBooksByAuthorUsingMyBatis(author);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/main/java/com/example/demo/entity/Author.java
Normal file
29
src/main/java/com/example/demo/entity/Author.java
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package com.example.demo.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@com.fasterxml.jackson.annotation.JsonIdentityInfo(
|
||||||
|
generator = com.fasterxml.jackson.annotation.ObjectIdGenerators.PropertyGenerator.class,
|
||||||
|
property = "id")
|
||||||
|
public class Author {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ManyToOne(cascade = CascadeType.ALL)
|
||||||
|
@JoinColumn(name = "region_id")
|
||||||
|
private Region region;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
|
||||||
|
@ToString.Exclude
|
||||||
|
private List<Book> books;
|
||||||
|
}
|
||||||
37
src/main/java/com/example/demo/entity/Book.java
Normal file
37
src/main/java/com/example/demo/entity/Book.java
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package com.example.demo.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@com.fasterxml.jackson.annotation.JsonIdentityInfo(
|
||||||
|
generator = com.fasterxml.jackson.annotation.ObjectIdGenerators.PropertyGenerator.class,
|
||||||
|
property = "id")
|
||||||
|
public class Book {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String title;
|
||||||
|
private String isbn;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "author_id")
|
||||||
|
private Author author;
|
||||||
|
|
||||||
|
@ManyToOne(cascade = CascadeType.ALL)
|
||||||
|
@JoinColumn(name = "publisher_id")
|
||||||
|
private Publisher publisher;
|
||||||
|
|
||||||
|
@Column(name = "publish_month")
|
||||||
|
private String publishMonth;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumns({
|
||||||
|
@JoinColumn(name = "publisher_id", referencedColumnName = "publisher_id", insertable = false, updatable = false),
|
||||||
|
@JoinColumn(name = "publish_month", referencedColumnName = "publish_month", insertable = false, updatable = false)
|
||||||
|
})
|
||||||
|
private Periodical periodical;
|
||||||
|
}
|
||||||
28
src/main/java/com/example/demo/entity/Periodical.java
Normal file
28
src/main/java/com/example/demo/entity/Periodical.java
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package com.example.demo.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@com.fasterxml.jackson.annotation.JsonIdentityInfo(
|
||||||
|
generator = com.fasterxml.jackson.annotation.ObjectIdGenerators.PropertyGenerator.class,
|
||||||
|
property = "id")
|
||||||
|
@Table(uniqueConstraints = {
|
||||||
|
@UniqueConstraint(columnNames = {"publisher_id", "publish_month"})
|
||||||
|
})
|
||||||
|
public class Periodical {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(name = "publish_month", nullable = false)
|
||||||
|
private String publishMonth; // Format: YYYY-MM
|
||||||
|
|
||||||
|
@ManyToOne(optional = false)
|
||||||
|
@JoinColumn(name = "publisher_id", nullable = false)
|
||||||
|
private Publisher publisher;
|
||||||
|
}
|
||||||
30
src/main/java/com/example/demo/entity/Publisher.java
Normal file
30
src/main/java/com/example/demo/entity/Publisher.java
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package com.example.demo.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@com.fasterxml.jackson.annotation.JsonIdentityInfo(
|
||||||
|
generator = com.fasterxml.jackson.annotation.ObjectIdGenerators.PropertyGenerator.class,
|
||||||
|
property = "id")
|
||||||
|
public class Publisher {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
@ManyToOne(cascade = CascadeType.ALL)
|
||||||
|
@JoinColumn(name = "region_id")
|
||||||
|
private Region region;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "publisher", cascade = CascadeType.ALL)
|
||||||
|
@ToString.Exclude
|
||||||
|
private List<Book> books;
|
||||||
|
}
|
||||||
18
src/main/java/com/example/demo/entity/Region.java
Normal file
18
src/main/java/com/example/demo/entity/Region.java
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package com.example.demo.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@com.fasterxml.jackson.annotation.JsonIdentityInfo(
|
||||||
|
generator = com.fasterxml.jackson.annotation.ObjectIdGenerators.PropertyGenerator.class,
|
||||||
|
property = "id")
|
||||||
|
public class Region {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
18
src/main/java/com/example/demo/mapper/BookMapper.java
Normal file
18
src/main/java/com/example/demo/mapper/BookMapper.java
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package com.example.demo.mapper;
|
||||||
|
|
||||||
|
import com.example.demo.entity.Book;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface BookMapper {
|
||||||
|
|
||||||
|
// Using Annotation
|
||||||
|
@Select("SELECT * FROM book")
|
||||||
|
List<Book> findAll();
|
||||||
|
|
||||||
|
// Using XML (defined in resources/mapper/BookMapper.xml)
|
||||||
|
List<Book> findByAuthorName(String authorName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import com.example.demo.entity.Author;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface AuthorRepository extends JpaRepository<Author, Long> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
import org.springframework.data.repository.NoRepositoryBean;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@NoRepositoryBean
|
||||||
|
public interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import com.example.demo.entity.Book;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface BookRepository extends BaseRepository<Book, Long> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
import jakarta.persistence.criteria.Path;
|
||||||
|
import jakarta.persistence.criteria.Predicate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class GenericSpecification {
|
||||||
|
|
||||||
|
public static <T> Specification<T> fromMap(Map<String, String> filters) {
|
||||||
|
return (root, query, criteriaBuilder) -> {
|
||||||
|
List<Predicate> predicates = new ArrayList<>();
|
||||||
|
|
||||||
|
filters.forEach((key, value) -> {
|
||||||
|
if (key.equals("fields") || key.equals("sort") || key.equals("page") || key.equals("size")) {
|
||||||
|
return; // Ignore reserved parameters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle nested properties (e.g., "author.name")
|
||||||
|
Path<String> path = null;
|
||||||
|
if (key.contains(".")) {
|
||||||
|
String[] parts = key.split("\\.");
|
||||||
|
path = root.get(parts[0]);
|
||||||
|
for (int i = 1; i < parts.length; i++) {
|
||||||
|
path = path.get(parts[i]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path = root.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Improved implementation:
|
||||||
|
// 1. If value contains "*", replace with "%" and use LIKE
|
||||||
|
// 2. Otherwise use EQUAL
|
||||||
|
if (path.getJavaType() == String.class && value.contains("*")) {
|
||||||
|
String likePattern = value.replace("*", "%");
|
||||||
|
predicates.add(criteriaBuilder.like(path, likePattern));
|
||||||
|
} else {
|
||||||
|
predicates.add(criteriaBuilder.equal(path, value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import com.example.demo.entity.Periodical;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface PeriodicalRepository extends JpaRepository<Periodical, Long> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import com.example.demo.entity.Publisher;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface PublisherRepository extends JpaRepository<Publisher, Long> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import com.example.demo.entity.Region;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface RegionRepository extends JpaRepository<Region, Long> {
|
||||||
|
}
|
||||||
80
src/main/java/com/example/demo/service/BookService.java
Normal file
80
src/main/java/com/example/demo/service/BookService.java
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import com.example.demo.entity.Author;
|
||||||
|
import com.example.demo.entity.Book;
|
||||||
|
import com.example.demo.entity.Periodical;
|
||||||
|
import com.example.demo.entity.Publisher;
|
||||||
|
import com.example.demo.entity.Region;
|
||||||
|
import com.example.demo.mapper.BookMapper;
|
||||||
|
import com.example.demo.repository.AuthorRepository;
|
||||||
|
import com.example.demo.repository.BookRepository;
|
||||||
|
import com.example.demo.repository.PeriodicalRepository;
|
||||||
|
import com.example.demo.repository.PublisherRepository;
|
||||||
|
import com.example.demo.repository.RegionRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BookService {
|
||||||
|
|
||||||
|
private final BookRepository bookRepository;
|
||||||
|
private final AuthorRepository authorRepository;
|
||||||
|
private final PublisherRepository publisherRepository;
|
||||||
|
private final RegionRepository regionRepository;
|
||||||
|
private final PeriodicalRepository periodicalRepository;
|
||||||
|
private final BookMapper bookMapper;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Book createBook(String title, String isbn, String authorName, String publisherName) {
|
||||||
|
Region region = new Region();
|
||||||
|
region.setName("Default Region");
|
||||||
|
region = regionRepository.save(region);
|
||||||
|
|
||||||
|
Author author = new Author();
|
||||||
|
author.setName(authorName);
|
||||||
|
author.setRegion(region);
|
||||||
|
author = authorRepository.save(author);
|
||||||
|
|
||||||
|
Publisher publisher = new Publisher();
|
||||||
|
publisher.setName(publisherName);
|
||||||
|
publisher.setRegion(region);
|
||||||
|
publisher = publisherRepository.save(publisher);
|
||||||
|
|
||||||
|
// Create a periodical matching this publisher and month
|
||||||
|
Periodical periodical = new Periodical();
|
||||||
|
periodical.setName("Monthly Journal " + publisherName);
|
||||||
|
periodical.setPublisher(publisher);
|
||||||
|
periodical.setPublishMonth("2023-01");
|
||||||
|
periodicalRepository.save(periodical);
|
||||||
|
|
||||||
|
Book book = new Book();
|
||||||
|
book.setTitle(title);
|
||||||
|
book.setIsbn(isbn);
|
||||||
|
book.setAuthor(author);
|
||||||
|
book.setPublisher(publisher);
|
||||||
|
book.setPublishMonth("2023-01"); // This should link to the periodical
|
||||||
|
book.setPeriodical(periodical);
|
||||||
|
|
||||||
|
return bookRepository.save(book);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Book> getAllBooksUsingJPA() {
|
||||||
|
return bookRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Book getBookById(Long id) {
|
||||||
|
return bookRepository.findById(id).orElseThrow(() -> new RuntimeException("Book not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Book> getAllBooksUsingMyBatis() {
|
||||||
|
return bookMapper.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Book> searchBooksByAuthorUsingMyBatis(String authorName) {
|
||||||
|
return bookMapper.findByAuthorName(authorName);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/main/resources/application.properties
Normal file
19
src/main/resources/application.properties
Normal 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
|
||||||
28
src/main/resources/mapper/BookMapper.xml
Normal file
28
src/main/resources/mapper/BookMapper.xml
Normal 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>
|
||||||
31
src/test/java/com/example/demo/BaseTest.java
Normal file
31
src/test/java/com/example/demo/BaseTest.java
Normal 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/test/java/com/example/demo/mapper/BookMapperTest.java
Normal file
31
src/test/java/com/example/demo/mapper/BookMapperTest.java
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
BIN
test_debug.log
Normal file
Binary file not shown.
BIN
test_output.log
Normal file
BIN
test_output.log
Normal file
Binary file not shown.
BIN
test_run.log
Normal file
BIN
test_run.log
Normal file
Binary file not shown.
Reference in New Issue
Block a user