Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
drop table user949 if exists;
drop table user_detail949 if exists;

create table user949
(
id integer NOT NULL PRIMARY KEY,
name varchar(32)
);

create table user_detail949
(
id integer NOT NULL PRIMARY KEY,
user_id integer,
email varchar(64)
);

INSERT INTO user949 (id, name) VALUES (1, 'user1');
INSERT INTO user949 (id, name) VALUES (2, 'user2');

INSERT INTO user_detail949 (id, user_id, email) VALUES (1, 1, 'user1@example.com');
INSERT INTO user_detail949 (id, user_id, email) VALUES (2, 2, 'user2@example.com');
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package tk.mybatis.mapper.issues._949_nested_query;

import org.apache.ibatis.session.SqlSession;
import org.junit.Assert;
import org.junit.Test;
import tk.mybatis.mapper.base.BaseTest;
import tk.mybatis.mapper.entity.Config;

import java.io.IOException;
import java.io.Reader;
import java.net.URL;
import java.util.List;

/**
* Test for issue #949: nested queries fail when enableBaseResultMapFlag is true.
*
* The root cause: setRawSqlSourceMapper was replacing ANY user-defined resultMap whose
* resultMappings list was empty (e.g. a resultMap with only a discriminator, or only
* nested-select associations whose column is shared). The fix is to only replace
* auto-generated inline resultMaps (ID ends with "-Inline", created from resultType=),
* never user-configured resultMaps (created from resultMap=).
*/
public class NestedQueryTest extends BaseTest {

@Override
protected Config getConfig() {
Config config = super.getConfig();
config.setEnableBaseResultMapFlag(true);
return config;
}

@Override
protected Reader getConfigFileAsReader() throws IOException {
URL url = NestedQueryTest.class.getResource("mybatis-config-issue949.xml");
return toReader(url);
}

@Override
protected Reader getSqlFileAsReader() throws IOException {
URL url = NestedQueryTest.class.getResource("CreateDB.sql");
return toReader(url);
}

/**
* A resultMap with explicit simple column mappings AND an association with nested select.
* resultMappings is NOT empty, so setRawSqlSourceMapper must not replace it.
* Verifies that the nested select (detail) is still executed.
*/
@Test
public void testNestedQueryWithExplicitMappings() {
SqlSession sqlSession = getSqlSession();
try {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> users = mapper.selectUsersWithDetail();

Assert.assertNotNull("Users should not be null", users);
Assert.assertEquals("Should have 2 users", 2, users.size());

for (User user : users) {
Assert.assertNotNull("User id should not be null", user.getId());
Assert.assertNotNull("User name should not be null", user.getName());
Assert.assertNotNull("User detail should not be null - nested select must have been executed",
user.getDetail());
Assert.assertNotNull("UserDetail email should not be null",
user.getDetail().getEmail());
}
} finally {
sqlSession.close();
}
}

/**
* A resultMap with ONLY an association (no explicit simple column mappings).
* resultMappings is NOT empty (contains the association), so setRawSqlSourceMapper
* must not replace it. The nested select (detail) must still be executed.
* Note: id is null because MyBatis auto-mapping skips "id" once it's consumed as
* the association column — that is expected behaviour for this resultMap configuration.
*/
@Test
public void testNestedQueryWithOnlyAssociation() {
SqlSession sqlSession = getSqlSession();
try {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> users = mapper.selectUsersWithDetailOnlyAssociation();

Assert.assertNotNull("Users should not be null", users);
Assert.assertEquals("Should have 2 users", 2, users.size());

for (User user : users) {
// The nested select (detail) should still be executed.
Assert.assertNotNull("User detail should not be null - nested select must have been executed",
user.getDetail());
Assert.assertNotNull("UserDetail email should not be null",
user.getDetail().getEmail());
}
} finally {
sqlSession.close();
}
}

/**
* Core bug scenario for issue #949.
*
* A user-defined resultMap with ONLY a discriminator has empty resultMappings.
* Before the fix, setRawSqlSourceMapper replaced it with the JPA-generated
* BaseMapperResultMap, wiping out the discriminator and breaking nested queries
* that lived inside the discriminator-case result maps.
*
* After the fix (only replace inline "-Inline" resultMaps), the discriminator
* result map is preserved:
* - user id=1 is routed to discriminatorCase1Map → nested select loads detail
* - user id=2 is routed to discriminatorCase2Map → no detail
*/
@Test
public void testNestedQueryWithDiscriminator() {
SqlSession sqlSession = getSqlSession();
try {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> users = mapper.selectUsersWithDiscriminator();

Assert.assertNotNull("Users should not be null", users);
Assert.assertEquals("Should have 2 users", 2, users.size());

// Find user1 and user2
User user1 = users.stream().filter(u -> u.getId() != null && u.getId() == 1).findFirst().orElse(null);
User user2 = users.stream().filter(u -> u.getId() != null && u.getId() == 2).findFirst().orElse(null);

Assert.assertNotNull("user1 should be found", user1);
Assert.assertNotNull("user2 should be found", user2);

// user1 goes through discriminatorCase1Map which has a nested select for detail
Assert.assertNotNull(
"user1 detail should be loaded by nested select via discriminator case 1 - " +
"this fails without the fix because the discriminator resultMap was replaced",
user1.getDetail());
Assert.assertEquals("user1@example.com", user1.getDetail().getEmail());

// user2 goes through discriminatorCase2Map which has no nested select
Assert.assertNull("user2 detail should be null (no nested select in case 2)", user2.getDetail());
} finally {
sqlSession.close();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package tk.mybatis.mapper.issues._949_nested_query;

import jakarta.persistence.Id;
import jakarta.persistence.Table;

/**
* Entity for issue 949 - nested query test
*/
@Table(name = "user949")
public class User {

@Id
private Integer id;

private String name;

private UserDetail detail;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public UserDetail getDetail() {
return detail;
}

public void setDetail(UserDetail detail) {
this.detail = detail;
}

@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "', detail=" + detail + "}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package tk.mybatis.mapper.issues._949_nested_query;

import jakarta.persistence.Column;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

/**
* Entity for issue 949 - nested query test
*/
@Table(name = "user_detail949")
public class UserDetail {

@Id
private Integer id;

@Column(name = "user_id")
private Integer userId;

private String email;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public Integer getUserId() {
return userId;
}

public void setUserId(Integer userId) {
this.userId = userId;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

@Override
public String toString() {
return "UserDetail{id=" + id + ", userId=" + userId + ", email='" + email + "'}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package tk.mybatis.mapper.issues._949_nested_query;

import tk.mybatis.mapper.common.BaseMapper;

import java.util.List;

/**
* Mapper for issue 949 - nested query test
*/
public interface UserMapper extends BaseMapper<User> {

/**
* Query using a resultMap that has an association with a nested select.
* With enableBaseResultMapFlag=true, this resultMap should NOT be replaced
* even though the entity class (User) is a JPA entity.
*/
List<User> selectUsersWithDetail();

/**
* Query using a resultMap that has ONLY an association (no explicit simple mappings).
* This tests the edge case where resultMappings may be non-empty but only with association.
*/
List<User> selectUsersWithDetailOnlyAssociation();

/**
* Query using a resultMap that has ONLY a discriminator (empty resultMappings).
* Without the fix, setRawSqlSourceMapper would replace this resultMap with the JPA-generated
* BaseMapperResultMap, losing the discriminator and breaking nested queries in case maps.
* With the fix, this user-defined resultMap is preserved as-is.
*/
List<User> selectUsersWithDiscriminator();

/**
* Nested select for UserDetail by userId.
*/
UserDetail selectDetailByUserId(Integer userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?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="tk.mybatis.mapper.issues._949_nested_query.UserMapper">

<!--
This resultMap has EXPLICIT simple column mappings AND an association with nested select.
resultMappings is NOT empty (has id, name, and association).
setRawSqlSourceMapper must NOT replace this resultMap.
-->
<resultMap id="userWithDetailMap" type="tk.mybatis.mapper.issues._949_nested_query.User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<association property="detail" column="id"
select="tk.mybatis.mapper.issues._949_nested_query.UserMapper.selectDetailByUserId"/>
</resultMap>

<!--
This resultMap has ONLY an association with nested select and NO explicit simple mappings.
resultMappings is NOT empty (has the association mapping).
setRawSqlSourceMapper must NOT replace this resultMap.
-->
<resultMap id="userWithDetailOnlyAssocMap" type="tk.mybatis.mapper.issues._949_nested_query.User">
<association property="detail" column="id"
select="tk.mybatis.mapper.issues._949_nested_query.UserMapper.selectDetailByUserId"/>
</resultMap>

<!--
This resultMap has ONLY a discriminator and NO explicit result mappings (resultMappings is empty).
This is the core bug scenario: setRawSqlSourceMapper was replacing this user-defined resultMap
with the JPA-generated BaseMapperResultMap, losing the discriminator entirely.
With the fix (check rm.getId().endsWith("-Inline")), this resultMap is preserved.
- user id=1 is routed to discriminatorCase1Map which loads the nested detail
- user id=2 is routed to discriminatorCase2Map which does not load detail
-->
<resultMap id="userDiscriminatorMap" type="tk.mybatis.mapper.issues._949_nested_query.User">
<discriminator javaType="int" column="id">
<case value="1" resultMap="discriminatorCase1Map"/>
<case value="2" resultMap="discriminatorCase2Map"/>
</discriminator>
</resultMap>

<!-- Case 1: loads user fields plus nested detail via nested select -->
<resultMap id="discriminatorCase1Map" type="tk.mybatis.mapper.issues._949_nested_query.User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<association property="detail" column="id"
select="tk.mybatis.mapper.issues._949_nested_query.UserMapper.selectDetailByUserId"/>
</resultMap>

<!-- Case 2: loads user fields only, no nested detail -->
<resultMap id="discriminatorCase2Map" type="tk.mybatis.mapper.issues._949_nested_query.User">
<id column="id" property="id"/>
<result column="name" property="name"/>
</resultMap>

<select id="selectUsersWithDetail" resultMap="userWithDetailMap">
SELECT id, name FROM user949
</select>

<select id="selectUsersWithDetailOnlyAssociation" resultMap="userWithDetailOnlyAssocMap">
SELECT id, name FROM user949
</select>

<select id="selectUsersWithDiscriminator" resultMap="userDiscriminatorMap">
SELECT id, name FROM user949
</select>

<select id="selectDetailByUserId" resultType="tk.mybatis.mapper.issues._949_nested_query.UserDetail">
SELECT id, user_id, email FROM user_detail949 WHERE user_id = #{userId}
</select>

</mapper>
Loading
Loading