From e27564d71c7f25ec36edfb038ef6a9baf2260532 Mon Sep 17 00:00:00 2001
From: Niaz Bin Siraj <niaz9767@gmail.com>
Date: Wed, 11 Sep 2024 22:47:57 +0600
Subject: [PATCH] Added selectMapWithList to handle non-unique column as a key

---
 .../result/DefaultMapResultHandler.java       |  8 +++
 .../org/apache/ibatis/session/SqlSession.java | 57 +++++++++++++++++++
 .../ibatis/session/SqlSessionManager.java     | 15 +++++
 .../session/defaults/DefaultSqlSession.java   | 23 +++++++-
 .../apache/ibatis/session/SqlSessionTest.java | 20 +++++++
 5 files changed, 121 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/apache/ibatis/executor/result/DefaultMapResultHandler.java b/src/main/java/org/apache/ibatis/executor/result/DefaultMapResultHandler.java
index f2ee92bcc02..f4171d35f8d 100644
--- a/src/main/java/org/apache/ibatis/executor/result/DefaultMapResultHandler.java
+++ b/src/main/java/org/apache/ibatis/executor/result/DefaultMapResultHandler.java
@@ -15,6 +15,7 @@
  */
 package org.apache.ibatis.executor.result;
 
+import java.util.List;
 import java.util.Map;
 
 import org.apache.ibatis.reflection.MetaObject;
@@ -30,6 +31,7 @@
 public class DefaultMapResultHandler<K, V> implements ResultHandler<V> {
 
   private final Map<K, V> mappedResults;
+  private final Map<K, List<V>> mappedResultsWithList;
   private final String mapKey;
   private final ObjectFactory objectFactory;
   private final ObjectWrapperFactory objectWrapperFactory;
@@ -42,6 +44,7 @@ public DefaultMapResultHandler(String mapKey, ObjectFactory objectFactory, Objec
     this.objectWrapperFactory = objectWrapperFactory;
     this.reflectorFactory = reflectorFactory;
     this.mappedResults = objectFactory.create(Map.class);
+    this.mappedResultsWithList = objectFactory.create(Map.class);
     this.mapKey = mapKey;
   }
 
@@ -52,9 +55,14 @@ public void handleResult(ResultContext<? extends V> context) {
     // TODO is that assignment always true?
     final K key = (K) mo.getValue(mapKey);
     mappedResults.put(key, value);
+    mappedResultsWithList.computeIfAbsent(key, k -> objectFactory.create(List.class)).add(value);
   }
 
   public Map<K, V> getMappedResults() {
     return mappedResults;
   }
+
+  public Map<K, List<V>> getMappedResultsWithList() {
+    return mappedResultsWithList;
+  }
 }
diff --git a/src/main/java/org/apache/ibatis/session/SqlSession.java b/src/main/java/org/apache/ibatis/session/SqlSession.java
index bb0f85932dc..6faa01f6150 100644
--- a/src/main/java/org/apache/ibatis/session/SqlSession.java
+++ b/src/main/java/org/apache/ibatis/session/SqlSession.java
@@ -156,6 +156,63 @@ public interface SqlSession extends Closeable {
    */
   <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds);
 
+  /**
+   * The selectMapWithList is a special case in that it is designed to convert a list of results into a Map based on one of the
+   * properties not needed to be unique in the resulting objects. Eg. Return a of Map[Integer,List<Author>] for selectMap("selectAuthors","id")
+   *
+   * @param <K>
+   *          the returned Map keys type
+   * @param <V>
+   *          the returned Map values type
+   * @param statement
+   *          Unique identifier matching the statement to use.
+   * @param mapKey
+   *          The property to use as key for each value in the list.
+   *
+   * @return Map containing key pair data.
+   */
+  <K, V> Map<K, List<V>> selectMapWithList(String statement, String mapKey);
+
+  /**
+   * The selectMap is a special case in that it is designed to convert a list of results into a Map based on one of the
+   * properties not needed to be unique in the resulting objects.
+   *
+   * @param <K>
+   *          the returned Map keys type
+   * @param <V>
+   *          the returned Map values type
+   * @param statement
+   *          Unique identifier matching the statement to use.
+   * @param parameter
+   *          A parameter object to pass to the statement.
+   * @param mapKey
+   *          The property to use as key for each value in the list.
+   *
+   * @return Map containing key pair data.
+   */
+  <K, V> Map<K, List<V>> selectMapWithList(String statement, Object parameter, String mapKey);
+
+  /**
+   * The selectMap is a special case in that it is designed to convert a list of results into a Map based on one of the
+   * properties not needed to be unique in the resulting objects.
+   *
+   * @param <K>
+   *          the returned Map keys type
+   * @param <V>
+   *          the returned Map values type
+   * @param statement
+   *          Unique identifier matching the statement to use.
+   * @param parameter
+   *          A parameter object to pass to the statement.
+   * @param mapKey
+   *          The property to use as key for each value in the list.
+   * @param rowBounds
+   *          Bounds to limit object retrieval
+   *
+   * @return Map containing key pair data.
+   */
+  <K, V> Map<K, List<V>> selectMapWithList(String statement, Object parameter, String mapKey, RowBounds rowBounds);
+
   /**
    * A Cursor offers the same results as a List, except it fetches data lazily using an Iterator.
    *
diff --git a/src/main/java/org/apache/ibatis/session/SqlSessionManager.java b/src/main/java/org/apache/ibatis/session/SqlSessionManager.java
index 183aa878f9c..2860ba34eba 100644
--- a/src/main/java/org/apache/ibatis/session/SqlSessionManager.java
+++ b/src/main/java/org/apache/ibatis/session/SqlSessionManager.java
@@ -179,6 +179,21 @@ public <K, V> Map<K, V> selectMap(String statement, Object parameter, String map
     return sqlSessionProxy.selectMap(statement, parameter, mapKey, rowBounds);
   }
 
+  @Override
+  public <K, V> Map<K, List<V>> selectMapWithList(String statement, String mapKey) {
+    return sqlSessionProxy.selectMapWithList(statement, mapKey);
+  }
+
+  @Override
+  public <K, V> Map<K, List<V>> selectMapWithList(String statement, Object parameter, String mapKey) {
+    return sqlSessionProxy.selectMapWithList(statement, parameter, mapKey);
+  }
+
+  @Override
+  public <K, V> Map<K, List<V>> selectMapWithList(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
+    return sqlSessionProxy.selectMapWithList(statement, parameter, mapKey, rowBounds);
+  }
+
   @Override
   public <T> Cursor<T> selectCursor(String statement) {
     return sqlSessionProxy.selectCursor(statement);
diff --git a/src/main/java/org/apache/ibatis/session/defaults/DefaultSqlSession.java b/src/main/java/org/apache/ibatis/session/defaults/DefaultSqlSession.java
index 62b39becea5..32e974ccd6a 100644
--- a/src/main/java/org/apache/ibatis/session/defaults/DefaultSqlSession.java
+++ b/src/main/java/org/apache/ibatis/session/defaults/DefaultSqlSession.java
@@ -96,15 +96,34 @@ public <K, V> Map<K, V> selectMap(String statement, Object parameter, String map
 
   @Override
   public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
+    return (Map<K, V>) executeSelectMap(statement, parameter, mapKey, rowBounds, false);
+  }
+
+  @Override
+  public <K, V> Map<K, List<V>> selectMapWithList(String statement, String mapKey) {
+    return this.selectMapWithList(statement, null, mapKey, RowBounds.DEFAULT);
+  }
+
+  @Override
+  public <K, V> Map<K, List<V>> selectMapWithList(String statement, Object parameter, String mapKey) {
+    return this.selectMapWithList(statement, parameter, mapKey, RowBounds.DEFAULT);
+  }
+
+  @Override
+  public <K, V> Map<K, List<V>> selectMapWithList(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
+    return (Map<K, List<V>>) executeSelectMap(statement, parameter, mapKey, rowBounds, true);
+  }
+
+  private <K, V> Map<K, ?> executeSelectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds, boolean useList) {
     final List<? extends V> list = selectList(statement, parameter, rowBounds);
     final DefaultMapResultHandler<K, V> mapResultHandler = new DefaultMapResultHandler<>(mapKey,
-        configuration.getObjectFactory(), configuration.getObjectWrapperFactory(), configuration.getReflectorFactory());
+      configuration.getObjectFactory(), configuration.getObjectWrapperFactory(), configuration.getReflectorFactory());
     final DefaultResultContext<V> context = new DefaultResultContext<>();
     for (V o : list) {
       context.nextResultObject(o);
       mapResultHandler.handleResult(context);
     }
-    return mapResultHandler.getMappedResults();
+    return useList ? mapResultHandler.getMappedResultsWithList() : mapResultHandler.getMappedResults();
   }
 
   @Override
diff --git a/src/test/java/org/apache/ibatis/session/SqlSessionTest.java b/src/test/java/org/apache/ibatis/session/SqlSessionTest.java
index 0b72e1d385e..65778709499 100644
--- a/src/test/java/org/apache/ibatis/session/SqlSessionTest.java
+++ b/src/test/java/org/apache/ibatis/session/SqlSessionTest.java
@@ -180,6 +180,26 @@ void shouldSelectAllAuthorsAsMap() {
     }
   }
 
+  @Test
+  void shouldSelectAllAuthorsAsMapWithList() {
+    try (SqlSession session = sqlMapper.openSession(TransactionIsolationLevel.SERIALIZABLE)) {
+      Author author = new Author(103, "jim", "******", "jim@somewhere.com", "Something...", null);
+      session.insert("org.apache.ibatis.domain.blog.mappers.AuthorMapper.insertAuthor", author);
+
+      final Map<String, List<Author>> authors = session
+        .selectMapWithList("org.apache.ibatis.domain.blog.mappers.AuthorMapper.selectAllAuthors", "username");
+      authors.forEach((k, v) -> {
+        if (k.equals("jim")) {
+          assertEquals(2, v.size());
+          v.forEach(a -> assertEquals("jim", a.getUsername()));
+        } else if (k.equals("sally")) {
+          assertEquals(1, v.size());
+          v.forEach(a -> assertEquals("sally", a.getUsername()));
+        }
+      });
+    }
+  }
+
   @Test
   void shouldSelectCountOfPosts() {
     try (SqlSession session = sqlMapper.openSession()) {