diff --git a/pom.xml b/pom.xml
index 9d65d49..2f3087b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,6 +35,21 @@
pom
import
+
+
+
+ org.mybatis.spring.boot
+ mybatis-spring-boot-starter-test
+ 3.0.3
+ test
+
+
+
+
+ com.h2database
+ h2
+ test
+
diff --git a/src/test/java/org/springblade/modules/martial/mapper/MapperIntegrationTest.java b/src/test/java/org/springblade/modules/martial/mapper/MapperIntegrationTest.java
new file mode 100644
index 0000000..52b902c
--- /dev/null
+++ b/src/test/java/org/springblade/modules/martial/mapper/MapperIntegrationTest.java
@@ -0,0 +1,200 @@
+package org.springblade.modules.martial.mapper;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Controller;
+import org.springframework.stereotype.Service;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.RestController;
+import org.springblade.modules.martial.pojo.entity.*;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Complete Integration Test - Real Database with MyBatis Plus
+ * Excludes Services/Controllers to avoid circular dependencies
+ * Tests ONLY Mapper layer with H2 database
+ */
+@SpringBootTest(classes = MapperIntegrationTest.TestConfig.class)
+@ActiveProfiles("test")
+@Transactional
+@DisplayName("Mapper Integration Tests - Complete Real Database Testing")
+class MapperIntegrationTest {
+
+ @Configuration
+ @EnableAutoConfiguration
+ @MapperScan("org.springblade.modules.martial.mapper")
+ @ComponentScan(
+ basePackages = "org.springblade",
+ excludeFilters = {
+ @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, RestController.class, Service.class})
+ }
+ )
+ static class TestConfig {
+ }
+
+ @Autowired
+ private MartialAthleteMapper athleteMapper;
+
+ @Autowired
+ private MartialTeamMapper teamMapper;
+
+ @Autowired
+ private MartialTeamMemberMapper teamMemberMapper;
+
+ @Autowired
+ private JdbcTemplate jdbcTemplate;
+
+ private Long testCompetitionId = 999L;
+
+ @BeforeEach
+ void setUp() {
+ // Insert test data
+ jdbcTemplate.update(
+ "INSERT INTO martial_competition (id, competition_name, start_date, end_date, status, is_deleted) " +
+ "VALUES (?, 'Integration Test', '2024-01-01', '2024-01-10', 1, 0)",
+ testCompetitionId
+ );
+
+ // Insert 30 athletes
+ for (int i = 1; i <= 30; i++) {
+ int team = ((i - 1) / 10) + 1;
+ jdbcTemplate.update(
+ "INSERT INTO martial_athlete (id, competition_id, project_id, player_name, organization, team_name, gender, is_deleted) " +
+ "VALUES (?, ?, 1, ?, ?, ?, 1, 0)",
+ i, testCompetitionId, "Athlete " + i, "Org " + team, "Team " + team
+ );
+ }
+
+ // Insert 3 teams
+ for (int team = 1; team <= 3; team++) {
+ jdbcTemplate.update(
+ "INSERT INTO martial_team (id, competition_id, team_name, is_deleted) VALUES (?, ?, ?, 0)",
+ team, testCompetitionId, "Team " + team
+ );
+ }
+
+ // Insert team members
+ int memberId = 1;
+ for (int team = 1; team <= 3; team++) {
+ for (int member = 1; member <= 5; member++) {
+ int athleteId = (team - 1) * 10 + member;
+ jdbcTemplate.update(
+ "INSERT INTO martial_team_member (id, team_id, athlete_id, is_deleted) VALUES (?, ?, ?, 0)",
+ memberId, team, athleteId
+ );
+ memberId++;
+ }
+ }
+ }
+
+ @Test
+ @DisplayName("Test 1: Batch query 30 athletes (1 query)")
+ void test1_batchQuery30Athletes() {
+ List ids = List.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L,
+ 11L, 12L, 13L, 14L, 15L, 16L, 17L, 18L, 19L, 20L,
+ 21L, 22L, 23L, 24L, 25L, 26L, 27L, 28L, 29L, 30L);
+
+ System.out.println("\n=== Test 1: Batch Query 30 Athletes ===");
+ long start = System.currentTimeMillis();
+ List athletes = athleteMapper.selectBatchIds(ids);
+ long time = System.currentTimeMillis() - start;
+
+ assertEquals(30, athletes.size());
+ System.out.println("✓ Returned 30 athletes in " + time + "ms (1 query)");
+ System.out.println("✓ Without optimization: would need 30 queries\n");
+ }
+
+ @Test
+ @DisplayName("Test 2: Complete N+1 optimization (4 queries total)")
+ void test2_completeN1Optimization() {
+ System.out.println("\n=== Test 2: Complete N+1 Optimization ===");
+ long totalStart = System.currentTimeMillis();
+
+ // Step 1: Batch query athletes
+ List athleteIds = List.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L);
+ long s1 = System.currentTimeMillis();
+ List athletes = athleteMapper.selectBatchIds(athleteIds);
+ long t1 = System.currentTimeMillis() - s1;
+
+ // Step 2: Batch query teams
+ List teamNames = athletes.stream().map(MartialAthlete::getTeamName).distinct().collect(Collectors.toList());
+ long s2 = System.currentTimeMillis();
+ QueryWrapper teamWrapper = new QueryWrapper<>();
+ teamWrapper.in("team_name", teamNames);
+ teamWrapper.eq("is_deleted", 0);
+ List teams = teamMapper.selectList(teamWrapper);
+ long t2 = System.currentTimeMillis() - s2;
+
+ // Step 3: Batch query team members
+ List teamIds = teams.stream().map(MartialTeam::getId).collect(Collectors.toList());
+ long s3 = System.currentTimeMillis();
+ QueryWrapper memberWrapper = new QueryWrapper<>();
+ memberWrapper.in("team_id", teamIds);
+ memberWrapper.eq("is_deleted", 0);
+ List members = teamMemberMapper.selectList(memberWrapper);
+ long t3 = System.currentTimeMillis() - s3;
+
+ // Step 4: Batch query member athletes
+ List memberAthleteIds = members.stream().map(MartialTeamMember::getAthleteId).collect(Collectors.toList());
+ long s4 = System.currentTimeMillis();
+ List memberAthletes = athleteMapper.selectBatchIds(memberAthleteIds);
+ long t4 = System.currentTimeMillis() - s4;
+
+ long total = System.currentTimeMillis() - totalStart;
+
+ assertEquals(10, athletes.size());
+ assertFalse(teams.isEmpty());
+ assertFalse(members.isEmpty());
+ assertFalse(memberAthletes.isEmpty());
+
+ System.out.println("Step 1 - Athletes: " + t1 + "ms");
+ System.out.println("Step 2 - Teams: " + t2 + "ms");
+ System.out.println("Step 3 - Members: " + t3 + "ms");
+ System.out.println("Step 4 - Member Athletes: " + t4 + "ms");
+ System.out.println("✓ Total: " + total + "ms (4 queries)");
+ System.out.println("✓ Without optimization: would need 30+ queries\n");
+ }
+
+ @Test
+ @DisplayName("Test 3: Performance - N+1 vs Batch")
+ void test3_performanceComparison() {
+ System.out.println("\n=== Test 3: Performance Comparison ===");
+
+ // N+1 approach
+ long n1Start = System.currentTimeMillis();
+ for (int i = 1; i <= 10; i++) {
+ athleteMapper.selectById((long) i);
+ }
+ long n1Time = System.currentTimeMillis() - n1Start;
+
+ // Batch approach
+ List ids = List.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L);
+ long batchStart = System.currentTimeMillis();
+ athleteMapper.selectBatchIds(ids);
+ long batchTime = System.currentTimeMillis() - batchStart;
+
+ System.out.println("N+1 (10 queries): " + n1Time + "ms");
+ System.out.println("Batch (1 query): " + batchTime + "ms");
+ if (n1Time > 0) {
+ System.out.println("Improvement: " + ((n1Time - batchTime) * 100 / n1Time) + "%");
+ }
+ System.out.println("✓ Batch is faster or equal\n");
+
+ assertTrue(batchTime <= n1Time || n1Time == 0);
+ }
+}
diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
index 71cfcf1..00b14d6 100644
--- a/src/test/resources/application-test.yml
+++ b/src/test/resources/application-test.yml
@@ -1,42 +1,38 @@
-#数据源配置
spring:
- data:
- redis:
- ##redis 单机环境配置
- ##将docker脚本部署的redis服务映射为宿主机ip
- ##生产环境推荐使用阿里云高可用redis服务并设置密码
- host: 127.0.0.1
- port: 6379
- password:
- database: 0
- ssl:
- enabled: false
- ##redis 集群环境配置
- #cluster:
- # nodes: 127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003
- # commandTimeout: 5000
+ application:
+ name: martial-master-test
+
datasource:
- url: jdbc:mysql://localhost:3306/bladex_boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
- username: root
- password: root
-
-#第三方登陆
-social:
- enabled: true
- domain: http://127.0.0.1:1888
-
-#blade配置
-blade:
- #分布式锁配置
- lock:
- ##是否启用分布式锁
+ driver-class-name: org.h2.Driver
+ url: jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;DATABASE_TO_UPPER=false
+ username: sa
+ password:
+
+ flyway:
enabled: false
- ##将docker脚本部署的redis服务映射为宿主机ip
- ##生产环境推荐使用阿里云高可用redis服务并设置密码
- address: redis://127.0.0.1:6379
- password: 123456
- #本地文件上传
- file:
- remote-mode: true
- upload-domain: http://localhost:8999
- remote-path: /usr/share/nginx/html
+
+ sql:
+ init:
+ mode: always
+ schema-locations: classpath:schema-test.sql
+ continue-on-error: false
+
+ cache:
+ type: simple
+
+ redis:
+ host: localhost
+ port: 6379
+
+ main:
+ allow-bean-definition-overriding: true
+ allow-circular-references: true
+
+mybatis-plus:
+ configuration:
+ log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+
+logging:
+ level:
+ org.springblade: INFO
+ org.springframework.jdbc: DEBUG
diff --git a/src/test/resources/data-test.sql b/src/test/resources/data-test.sql
new file mode 100644
index 0000000..e69de29
diff --git a/src/test/resources/schema-test.sql b/src/test/resources/schema-test.sql
new file mode 100644
index 0000000..80faa06
--- /dev/null
+++ b/src/test/resources/schema-test.sql
@@ -0,0 +1,171 @@
+-- Test database schema for H2
+
+CREATE TABLE IF NOT EXISTS martial_competition (
+ id BIGINT PRIMARY KEY,
+ competition_name VARCHAR(255),
+ start_date DATE,
+ end_date DATE,
+ status INT,
+ tenant_id VARCHAR(12) DEFAULT '000000',
+ create_user BIGINT,
+ create_dept BIGINT,
+ create_time DATETIME,
+ update_user BIGINT,
+ update_time DATETIME,
+ is_deleted INT DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS martial_athlete (
+ id BIGINT PRIMARY KEY,
+ order_id BIGINT,
+ competition_id BIGINT,
+ project_id BIGINT,
+ player_name VARCHAR(255),
+ player_no VARCHAR(50),
+ gender INT,
+ age INT,
+ id_card VARCHAR(50),
+ id_card_type VARCHAR(50),
+ birth_date DATE,
+ nation VARCHAR(50),
+ contact_phone VARCHAR(50),
+ organization VARCHAR(255),
+ organization_type VARCHAR(50),
+ team_name VARCHAR(255),
+ category VARCHAR(50),
+ order_num INT,
+ introduction TEXT,
+ attachments TEXT,
+ photo_url VARCHAR(500),
+ registration_status INT,
+ competition_status INT,
+ total_score DECIMAL(10,2),
+ ranking INT,
+ remark TEXT,
+ tenant_id VARCHAR(12) DEFAULT '000000',
+ create_user BIGINT,
+ create_dept BIGINT,
+ create_time DATETIME,
+ update_user BIGINT,
+ update_time DATETIME,
+ status INT DEFAULT 1,
+ is_deleted INT DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS martial_team (
+ id BIGINT PRIMARY KEY,
+ competition_id BIGINT,
+ team_name VARCHAR(255),
+ organization VARCHAR(255),
+ contact_person VARCHAR(100),
+ contact_phone VARCHAR(50),
+ total_score DECIMAL(10,2),
+ ranking INT,
+ remark TEXT,
+ member_count INT,
+ tenant_id VARCHAR(12) DEFAULT '000000',
+ create_user BIGINT,
+ create_dept BIGINT,
+ create_time DATETIME,
+ update_user BIGINT,
+ update_time DATETIME,
+ status INT DEFAULT 1,
+ is_deleted INT DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS martial_team_member (
+ id BIGINT PRIMARY KEY,
+ team_id BIGINT,
+ athlete_id BIGINT,
+ member_role VARCHAR(50),
+ tenant_id VARCHAR(12) DEFAULT '000000',
+ create_user BIGINT,
+ create_dept BIGINT,
+ create_time DATETIME,
+ update_user BIGINT,
+ update_time DATETIME,
+ status INT DEFAULT 1,
+ is_deleted INT DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS martial_schedule (
+ id BIGINT PRIMARY KEY,
+ competition_id BIGINT,
+ project_id BIGINT,
+ schedule_date DATE,
+ schedule_time TIME,
+ venue VARCHAR(255),
+ status INT,
+ tenant_id VARCHAR(12) DEFAULT '000000',
+ create_user BIGINT,
+ create_dept BIGINT,
+ create_time DATETIME,
+ update_user BIGINT,
+ update_time DATETIME,
+ is_deleted INT DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS martial_schedule_group (
+ id BIGINT PRIMARY KEY,
+ schedule_id BIGINT,
+ group_name VARCHAR(100),
+ group_order INT,
+ status INT,
+ tenant_id VARCHAR(12) DEFAULT '000000',
+ create_user BIGINT,
+ create_dept BIGINT,
+ create_time DATETIME,
+ update_user BIGINT,
+ update_time DATETIME,
+ is_deleted INT DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS martial_schedule_athlete (
+ id BIGINT PRIMARY KEY,
+ schedule_id BIGINT,
+ group_id BIGINT,
+ athlete_id BIGINT,
+ appearance_order INT,
+ status INT,
+ tenant_id VARCHAR(12) DEFAULT '000000',
+ create_user BIGINT,
+ create_dept BIGINT,
+ create_time DATETIME,
+ update_user BIGINT,
+ update_time DATETIME,
+ is_deleted INT DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS martial_project (
+ id BIGINT PRIMARY KEY,
+ competition_id BIGINT,
+ project_name VARCHAR(255),
+ project_type VARCHAR(50),
+ description TEXT,
+ tenant_id VARCHAR(12) DEFAULT '000000',
+ create_user BIGINT,
+ create_dept BIGINT,
+ create_time DATETIME,
+ update_user BIGINT,
+ update_time DATETIME,
+ status INT DEFAULT 1,
+ is_deleted INT DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS martial_score (
+ id BIGINT PRIMARY KEY,
+ competition_id BIGINT,
+ athlete_id BIGINT,
+ project_id BIGINT,
+ judge_id BIGINT,
+ score DECIMAL(10,2),
+ score_type VARCHAR(50),
+ tenant_id VARCHAR(12) DEFAULT '000000',
+ create_user BIGINT,
+ create_dept BIGINT,
+ create_time DATETIME,
+ update_user BIGINT,
+ update_time DATETIME,
+ status INT DEFAULT 1,
+ is_deleted INT DEFAULT 0
+);