Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Missing rows when using @Arg and nested objects #3088

Open
dirk-tf opened this issue Feb 6, 2024 · 3 comments
Open

Missing rows when using @Arg and nested objects #3088

dirk-tf opened this issue Feb 6, 2024 · 3 comments

Comments

@dirk-tf
Copy link

dirk-tf commented Feb 6, 2024

Hi, i'm trying to have my query return an object structure. In a similar way ConstructorColumnPrefixTest does, as multiple nested authors. But we had data/rows missing. From a quick glance, DefaultResultSetHandler.createRowKey seems... lacking. But i don't fully grasp it. Is this a bug or am I doing something unintended?

I've created a test case created with the current mybatis-3 master. The queries/objects are somewhat nonsensical, but the key point is the second should return another copy with a different int but doesnt.

MyBatis version

tested 3.5.15 and master

Database vendor and version

Both oracle and whatever the test uses, thus probably irrelevant.

Test case or example project

package org.apache.ibatis.submitted;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.sql.Timestamp;
import java.util.List;

import org.apache.ibatis.BaseDataTest;
import org.apache.ibatis.annotations.Arg;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.junit.jupiter.api.Test;

public class ConstructorArgsMissingRows {

  record Section(String section, Timestamp createdOn, Integer someInt) {}
  record Post(Section section, String subject) { }

  interface Mapper {
    @Results(id = "SectionResultMap")
    @Arg(column = "SECTION", name = "section", javaType = String.class)
    @Arg(column = "CREATED_ON", name = "createdOn", javaType = Timestamp.class)
    @Arg(column = "SOME_INT", name = "someInt", javaType = Integer.class)
    @Select("SELECT post.*, 1 AS SOME_INT FROM post")
    List<Section> getOnce();

    @Select("""
    SELECT SECTION AS PRE_SECTION, CREATED_ON AS PRE_CREATED_ON, SUBJECT, 1 AS PRE_SOME_INT FROM post
    UNION
    SELECT SECTION AS PRE_SECTION, CREATED_ON AS PRE_CREATED_ON, SUBJECT, 2 AS PRE_SOME_INT FROM post
    """)
    @Arg(resultMap = "SectionResultMap", name = "section", columnPrefix = "PRE_", javaType = Section.class)
    @Arg(column = "SUBJECT", name = "subject", javaType = String.class)
    List<Post> getTwice();
  }

  @Test()
  void shouldGetData() throws Exception {
    final Environment environment = new Environment("test", new JdbcTransactionFactory(),
      BaseDataTest.createBlogDataSource());
    final Configuration config = new Configuration(environment);
    config.addMapper(Mapper.class);
    var sqlSessionFactory = new SqlSessionFactoryBuilder().build(config);
    try (SqlSession session = sqlSessionFactory.openSession()) {
      Mapper mapper = session.getMapper(Mapper.class);
      var onceResult = mapper.getOnce();
      var twiceResult = mapper.getTwice();
      assertEquals(onceResult.size() * 2, twiceResult.size());
    }
  }
}

Steps to reproduce

run test above

Expected result

getTwice returns each row twice, with a different SOME_INT.

Actual result

Only set (5 rows right now) are returned. Actually, it works (10 rows) if you remove "String subject" from Post + the related @arg, confusing me even more

@harawata
Copy link
Member

harawata commented Feb 6, 2024

Hello @dirk-tf ,

It is not a bug.
You need to specify id in the parent object, at least.
In a realistic scenario, it would be POST.ID, but it does not work with your test case because of UNION obviously.

Note that there does not have to be a property in Post object.
Assuming that ID is the primary key of POST, you can add the following annotation to tell MyBatis to use ID to identify the parent object.

@Result(id = true, column = "ID")

Specifying id for the nested Section could improve performance.
When there is no id, MyBatis uses the combination of all the (non-complex) properties to identify the object. This is why it works when there is no subject (i.e. when there is subject, its value is used to identify the parent object).

@dirk-tf
Copy link
Author

dirk-tf commented Feb 7, 2024

Oh, so just have to mark columns necessary for an ID. Ok i can work with that. Thanks alot.

Though I really don't like the "un-configured default" potentially silently dropping rows.

Re "subject", didn't fully understand you, but re-checking the code with the "non-complex" knowledge: removing it leaves none, disabling the cache.

I actually already tried to set id=true on all @Args of both queries of test posted above. It didn't work. Setting any ids on "SectionResultMap" doesn't seem to affect "getTwice". Should it?

Again huge thanks for you help, @harawata

@harawata
Copy link
Member

harawata commented Feb 7, 2024

I know the behavior confuses users ( #512 #522 #580 #1848 ).
This is because MyBatis handles 1:1 mapping the same way as 1:N mapping, so it's by design, basically.

Specifying id on SectionResultMap may not change the result, but it could affect performance.
Simply put, if id is not specified, MyBatis has to create a "row key" from values of SECTION, CREATED_ON and SOME_INT to identify Section.
It should not make much difference if the number of rows returned from the query is small, though.

You can use @Arg(id =true) instead of @Result(id = true), but then you may have to add a constructor argument which probably is not what you want.

@Arg(id = true, name = "id", javaType = Integer.class)
@Arg(resultMap = "SectionResultMap", name = "section", columnPrefix = "PRE_", javaType = Section.class)
@Arg(column = "SUBJECT", name = "subject", javaType = String.class)
List<Post> getTwice();
record Post(Integer id, Section section, String subject) { }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants