diff --git a/src/main/java/jp/co/future/uroborosql/SqlQueryImpl.java b/src/main/java/jp/co/future/uroborosql/SqlQueryImpl.java index b848a229..17107080 100644 --- a/src/main/java/jp/co/future/uroborosql/SqlQueryImpl.java +++ b/src/main/java/jp/co/future/uroborosql/SqlQueryImpl.java @@ -18,6 +18,7 @@ import jp.co.future.uroborosql.converter.EntityResultSetConverter; import jp.co.future.uroborosql.converter.MapResultSetConverter; import jp.co.future.uroborosql.converter.ResultSetConverter; +import jp.co.future.uroborosql.converter.SingleColumnResultSetConverter; import jp.co.future.uroborosql.exception.DataNonUniqueException; import jp.co.future.uroborosql.exception.DataNotFoundException; import jp.co.future.uroborosql.exception.UroborosqlSQLException; @@ -258,8 +259,42 @@ public Stream> stream(final CaseFormat caseFormat) { */ @Override public Stream stream(final Class type) { - return stream( - new EntityResultSetConverter<>(type, new PropertyMapperManager(this.agent.getSqlConfig().getClock()))); + if (type == null) { + throw new IllegalArgumentException("Argument 'type' is required."); + } + PropertyMapperManager manager = new PropertyMapperManager(this.agent.getSqlConfig().getClock()); + if (SingleColumnResultSetConverter.accept(type)) { + return stream(new SingleColumnResultSetConverter<>(null, type, manager)); + } else { + return stream(new EntityResultSetConverter<>(type, manager)); + } + } + + /** + * {@inheritDoc} + * + * @see jp.co.future.uroborosql.fluent.SqlQuery#select(java.lang.Class) + */ + @Override + public Stream select(Class type) { + return select(null, type); + } + + /** + * {@inheritDoc} + * + * @see jp.co.future.uroborosql.fluent.SqlQuery#select(java.lang.String, java.lang.Class) + */ + @Override + public Stream select(String col, Class type) { + if (type == null) { + throw new IllegalArgumentException("Argument 'type' is required."); + } + if (!SingleColumnResultSetConverter.accept(type)) { + throw new IllegalArgumentException(type.getName() + " is not supported."); + } + return stream(new SingleColumnResultSetConverter<>(col, type, + new PropertyMapperManager(this.agent.getSqlConfig().getClock()))); } } diff --git a/src/main/java/jp/co/future/uroborosql/converter/EntityResultSetConverter.java b/src/main/java/jp/co/future/uroborosql/converter/EntityResultSetConverter.java index 8a2e5129..1cb7db90 100644 --- a/src/main/java/jp/co/future/uroborosql/converter/EntityResultSetConverter.java +++ b/src/main/java/jp/co/future/uroborosql/converter/EntityResultSetConverter.java @@ -54,7 +54,7 @@ public EntityResultSetConverter(final Class entityType, final Prope try { this.constructor = (Constructor) entityType.getConstructor(); } catch (NoSuchMethodException e) { - throw new UroborosqlRuntimeException(e); + throw new UroborosqlRuntimeException("EntityType should have a default constructor.", e); } this.mappingColumnMap = Arrays.stream(MappingUtils.getMappingColumns(entityType)) diff --git a/src/main/java/jp/co/future/uroborosql/converter/SingleColumnResultSetConverter.java b/src/main/java/jp/co/future/uroborosql/converter/SingleColumnResultSetConverter.java new file mode 100644 index 00000000..56809762 --- /dev/null +++ b/src/main/java/jp/co/future/uroborosql/converter/SingleColumnResultSetConverter.java @@ -0,0 +1,153 @@ +/** + * Copyright (c) 2017-present, Future Corporation + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +package jp.co.future.uroborosql.converter; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZonedDateTime; +import java.util.Date; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jp.co.future.uroborosql.exception.UroborosqlRuntimeException; +import jp.co.future.uroborosql.mapping.JavaType; +import jp.co.future.uroborosql.mapping.mapper.PropertyMapperManager; +import jp.co.future.uroborosql.utils.CaseFormat; +import jp.co.future.uroborosql.utils.StringUtils; + +/** + * 検索結果の1行をプリミティブ型(のラッパークラス)に変換する変換器 + * + * @param プリミティブ型のラッパークラス、またはString型 + * @author H.Sugimoto + * + */ +public class SingleColumnResultSetConverter implements ResultSetConverter { + /** ロガー */ + private static final Logger LOG = LoggerFactory.getLogger(SingleColumnResultSetConverter.class); + + private final String col; + private final JavaType javaType; + private final PropertyMapperManager mapperManager; + private int columnPosition; + + /** + * 受付可能な型かどうかを判定する. + * + * @param type 判定対象の型 + * @return 受付可能な場合true を返す. + */ + public static final boolean accept(Class type) { + if (type == null) { + return false; + } else if (String.class.equals(type) || + boolean.class.equals(type) || + Boolean.class.equals(type) || + byte.class.equals(type) || + Byte.class.equals(type) || + short.class.equals(type) || + Short.class.equals(type) || + int.class.equals(type) || + Integer.class.equals(type) || + long.class.equals(type) || + Long.class.equals(type) || + float.class.equals(type) || + Float.class.equals(type) || + double.class.equals(type) || + Double.class.equals(type) || + BigInteger.class.equals(type) || + BigDecimal.class.equals(type)) { + // 基本的な型は受付可能とする + return true; + } else if (Date.class.isAssignableFrom(type)) { + // Date型かDate型を継承するクラス(Timestampなど)は受付可能とする + return true; + } else if (LocalDateTime.class.equals(type) || + OffsetDateTime.class.equals(type) || + ZonedDateTime.class.equals(type) || + LocalDate.class.equals(type) || + LocalTime.class.equals(type) || + OffsetTime.class.equals(type) || + Year.class.equals(type) || + YearMonth.class.equals(type) || + MonthDay.class.equals(type) || + Month.class.equals(type) || + DayOfWeek.class.equals(type)) { + // java.time配下の時間オブジェクトは受付可能とする + return true; + } else if (Object[].class.equals(type) || + byte[].class.equals(type)) { + // 配列型でコンポーネント型が受入対象の型である場合は受入可能とする + return true; + } else { + return false; + } + } + + /** + * コンストラクタ + * + * @param col 取得対象のカラム名. null の場合は先頭カラムを取得 + * @param columnType カラムの型 + * @param mapperManager PropertyMapperManager + */ + public SingleColumnResultSetConverter(String col, final Class columnType, + final PropertyMapperManager mapperManager) { + this.col = CaseFormat.UPPER_SNAKE_CASE.convert(col); + this.javaType = JavaType.of(columnType); + this.mapperManager = mapperManager; + this.columnPosition = StringUtils.isEmpty(col) ? 1 : -1; + } + + /** + * {@inheritDoc} + * + * @see jp.co.future.uroborosql.converter.ResultSetConverter#createRecord(java.sql.ResultSet) + */ + @SuppressWarnings("unchecked") + @Override + public E createRecord(final ResultSet rs) throws SQLException { + try { + if (this.columnPosition == -1) { + ResultSetMetaData rsmd = rs.getMetaData(); + int columnCount = rsmd.getColumnCount(); + + // 指定されたカラムのpositionを取得 + for (int i = 1; i <= columnCount; i++) { + String columnLabel = CaseFormat.UPPER_SNAKE_CASE.convert(rsmd.getColumnLabel(i)); + if (col.equalsIgnoreCase(columnLabel)) { + this.columnPosition = i; + break; + } + } + if (this.columnPosition == -1) { + // 指定されたカラムが見つからない場合は例外をスローする + throw new UroborosqlRuntimeException(col + " not found in query result."); + } + } + + return (E) mapperManager.getValue(this.javaType, rs, this.columnPosition); + } catch (SQLException | RuntimeException | Error e) { + LOG.error("Error!!", e); + throw e; + } + } +} diff --git a/src/main/java/jp/co/future/uroborosql/fluent/SqlQuery.java b/src/main/java/jp/co/future/uroborosql/fluent/SqlQuery.java index 633cd202..9a547df0 100644 --- a/src/main/java/jp/co/future/uroborosql/fluent/SqlQuery.java +++ b/src/main/java/jp/co/future/uroborosql/fluent/SqlQuery.java @@ -173,7 +173,7 @@ public interface SqlQuery extends SqlFluent { List collect(Class type); /** - * 検索結果をStreamとして取得(終端処理) + * 検索結果をStreamとして取得 * * @param Streamの型 * @param converter ResultSetの各行を変換するための変換器 @@ -182,14 +182,14 @@ public interface SqlQuery extends SqlFluent { Stream stream(ResultSetConverter converter); /** - * 検索結果をMapのStreamとして取得(終端処理) + * 検索結果をMapのStreamとして取得 * * @return 検索結果を順次取得するStream */ Stream> stream(); /** - * 検索結果をMapのStreamとして取得(終端処理) + * 検索結果をMapのStreamとして取得 * * @param caseFormat Mapのキーの変換書式 * @return 検索結果を順次取得するStream @@ -197,11 +197,31 @@ public interface SqlQuery extends SqlFluent { Stream> stream(CaseFormat caseFormat); /** - * 検索結果をEntityのStreamとして取得(終端処理) + * 検索結果をEntityのStreamとして取得 * * @param Streamの型 * @param type 受け取りたいEntityの型 * @return 検索結果を順次取得するStream */ Stream stream(Class type); + + /** + * 検索結果の先頭カラムをStreamとして取得 + * + * @param Streamの型 + * @param type 取得するカラムの型 + * @return 検索結果を順次取得するStream + */ + Stream select(Class type); + + /** + * 検索結果の指定したカラムをStreamとして取得 + * + * @param Streamの型 + * @param col 取得するカラムの名前 + * @param type 取得するカラムの型 + * @return 検索結果を順次取得するStream + */ + Stream select(String col, Class type); + } diff --git a/src/test/java/jp/co/future/uroborosql/SqlQueryTest.java b/src/test/java/jp/co/future/uroborosql/SqlQueryTest.java index 6a9fffcd..3c3a9dc5 100644 --- a/src/test/java/jp/co/future/uroborosql/SqlQueryTest.java +++ b/src/test/java/jp/co/future/uroborosql/SqlQueryTest.java @@ -1,16 +1,37 @@ package jp.co.future.uroborosql; -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.math.BigDecimal; +import java.math.BigInteger; import java.nio.file.Paths; import java.sql.ResultSet; import java.sql.ResultSetMetaData; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.DayOfWeek; import java.time.Duration; import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.Date; import java.util.HashSet; import java.util.LinkedHashMap; @@ -18,6 +39,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.Test; @@ -303,6 +325,20 @@ public void testQueryFluentCollectEntity() throws Exception { assertEquals("0番目の商品", product.getProductDescription()); } + /** + * クエリ実行処理のテストケース(Fluent API)。 + */ + @Test + public void testQueryFluentCollectSingleType() throws Exception { + // 事前条件 + cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv")); + + List ans = agent.query("example/select_product").param("product_id", Arrays.asList(0, 1, 2, 3)) + .collect(Integer.class); + assertEquals("結果の件数が一致しません。", 2, ans.size()); + assertThat(ans.get(0), is(0)); + } + /** * クエリ実行処理のテストケース(Fluent API)。 */ @@ -738,6 +774,19 @@ public void testQueryFluentFirstByClass() throws Exception { assertEquals("0番目の商品", product.getProductDescription()); } + /** + * クエリ実行処理(1件取得)のテストケース(Fluent API)。 + */ + @Test + public void testQueryFluentFirstSingleType() throws Exception { + // 事前条件 + cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv")); + + Integer ans = agent.query("example/select_product") + .first(Integer.class); + assertThat(ans, is(0)); + } + /** * クエリ実行処理(1件取得:Optional)のテストケース(Fluent API)。 */ @@ -767,6 +816,19 @@ public void testQueryFluentFindFirstByClass() throws Exception { assertFalse(optional.isPresent()); } + /** + * クエリ実行処理(1件取得:Optional)のテストケース(Fluent API)。 + */ + @Test + public void testQueryFluentFindFirstSingleType() throws Exception { + // 事前条件 + cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv")); + + Optional ans = agent.query("example/select_product") + .findFirst(Integer.class); + assertThat(ans.orElse(null), is(0)); + } + /** * クエリ実行処理(1件取得)のテストケース(Fluent API)。 */ @@ -801,6 +863,20 @@ public void testQueryFluentOneByClass() throws Exception { assertEquals("0番目の商品", product.getProductDescription()); } + /** + * クエリ実行処理(1件取得)のテストケース(Fluent API)。 + */ + @Test + public void testQueryFluentOneByClassSingleType() throws Exception { + // 事前条件 + cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv")); + Integer productId = agent.query("example/select_product") + .param("product_id", Arrays.asList(0)) + .one(Integer.class); + + assertThat(productId, is(0)); + } + /** * クエリ実行処理(1件取得:Optional)のテストケース(Fluent API)。 */ @@ -837,6 +913,20 @@ public void testQueryFluentFindOneByClass() throws Exception { assertFalse(optional.isPresent()); } + /** + * クエリ実行処理(1件取得:Optional)のテストケース(Fluent API)。 + */ + @Test + public void testQueryFluentFindOneByClassSingleType() throws Exception { + // 事前条件 + cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv")); + Optional productId = agent.query("example/select_product") + .param("product_id", Arrays.asList(0)) + .findOne(Integer.class); + + assertThat(productId.orElse(null), is(0)); + } + /** * クエリ実行処理のテストケース。 */ @@ -1000,6 +1090,167 @@ public void testQueryFluentStreamEntity() throws Exception { }); assertThat(agent.query("example/select_product").param("product_id", Arrays.asList(0, 1)).stream().count(), is(2L)); + + try { + agent.query("example/select_product").stream((Class) null); + assertTrue(false); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), is("Argument 'type' is required.")); + } catch (Exception ex) { + assertTrue(false); + } + } + + /** + * クエリ実行処理のテストケース(Fluent API)。 + */ + @Test + public void testQueryFluentStreamSingleType() throws Exception { + // 事前条件 + cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv")); + + List results = agent.query("example/select_product") + .stream(Integer.class) + .collect(Collectors.toList()); + assertThat(results.size(), is(2)); + assertThat(results.get(0), is(0)); + assertThat(results.get(1), is(1)); + + List stringResults = agent.query("example/select_product") + .stream(String.class) + .collect(Collectors.toList()); + assertThat(stringResults.size(), is(2)); + } + + /** + * クエリ実行(1カラム)処理のテストケース(Fluent API)。 + */ + @Test + public void testQueryFluentSelect() throws Exception { + // 事前条件 + cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv")); + + // 先頭カラムの取得 + List idResults = agent.query("example/select_product") + .select(Integer.class) + .collect(Collectors.toList()); + assertThat(idResults.size(), is(2)); + assertThat(idResults.get(0), is(0)); + assertThat(idResults.get(1), is(1)); + + // カラム指定の取得 + List nameResults = agent.query("example/select_product") + .select("productName", String.class) + .collect(Collectors.toList()); + assertThat(nameResults.size(), is(2)); + assertThat(nameResults.get(0), is("商品名0")); + assertThat(nameResults.get(1), is("商品名1")); + + try { + // 存在しないカラムの指定 + agent.query("example/select_product") + .select("productNameNothing", String.class) + .collect(Collectors.toList()); + assertTrue(false); + } catch (UroborosqlRuntimeException ex) { + assertThat(ex.getMessage(), is("PRODUCT_NAME_NOTHING not found in query result.")); + } catch (Exception ex) { + assertTrue(false); + } + } + + /** + * クエリ実行(1カラム)処理のテストケース(Fluent API)。 + */ + @Test + public void testQueryFluentSelectByType() throws Exception { + // 基本の型 + assertThat(agent.queryWith("select 'abc'").select(String.class).findFirst().orElse(null), is("abc")); + assertThat(agent.queryWith("select true").select(boolean.class).findFirst().orElse(null), is(true)); + assertThat(agent.queryWith("select false").select(boolean.class).findFirst().orElse(null), is(false)); + assertThat(agent.queryWith("select true").select(Boolean.class).findFirst().orElse(null), is(true)); + assertThat(agent.queryWith("select false").select(Boolean.class).findFirst().orElse(null), is(false)); + assertThat(agent.queryWith("select X'61'").select(byte.class).findFirst().orElse(null), is("a".getBytes()[0])); + assertThat(agent.queryWith("select X'61'").select(Byte.class).findFirst().orElse(null), is("a".getBytes()[0])); + assertThat(agent.queryWith("select 1").select(short.class).findFirst().orElse(null), is((short) 1)); + assertThat(agent.queryWith("select 1").select(Short.class).findFirst().orElse(null), is((short) 1)); + assertThat(agent.queryWith("select 1").select(int.class).findFirst().orElse(null), is(1)); + assertThat(agent.queryWith("select 1").select(Integer.class).findFirst().orElse(null), is(1)); + assertThat(agent.queryWith("select 10000000000").select(long.class).findFirst().orElse(null), is(10000000000L)); + assertThat(agent.queryWith("select 10000000000").select(Long.class).findFirst().orElse(null), is(10000000000L)); + assertThat(agent.queryWith("select 1000.123").select(float.class).findFirst().orElse(null), is(1000.123f)); + assertThat(agent.queryWith("select 1000.123").select(Float.class).findFirst().orElse(null), is(1000.123f)); + assertThat(agent.queryWith("select 10000000000.123").select(double.class).findFirst().orElse(null), + is(10000000000.123d)); + assertThat(agent.queryWith("select 10000000000.123").select(Double.class).findFirst().orElse(null), + is(10000000000.123d)); + assertThat(agent.queryWith("select 1000000000000").select(BigInteger.class).findFirst().orElse(null), + is(new BigInteger("1000000000000"))); + assertThat(agent.queryWith("select 10000000000.123").select(BigDecimal.class).findFirst().orElse(null), + is(new BigDecimal("10000000000.123"))); + + // 日付型 + assertThat( + agent.queryWith("select CURRENT_DATE").select(java.sql.Date.class).findFirst().orElse(null).toString(), + is(new java.sql.Date(Calendar.getInstance().getTimeInMillis()).toString())); + assertThat( + agent.queryWith("select CURRENT_DATE").select(Date.class).findFirst().orElse(null), + instanceOf(Date.class)); + assertThat( + agent.queryWith("select CURRENT_TIME").select(Time.class).findFirst().orElse(null), + instanceOf(Time.class)); + assertThat(agent.queryWith("select CURRENT_TIMESTAMP").select(Timestamp.class).findFirst().orElse(null), + instanceOf(Timestamp.class)); + + // java.time API + assertThat(agent.queryWith("select CURRENT_DATE").select(LocalDate.class).findFirst().orElse(null), + is(LocalDate.now())); + assertThat(agent.queryWith("select CURRENT_TIME").select(LocalTime.class).findFirst().orElse(null), + instanceOf(LocalTime.class)); + assertThat(agent.queryWith("select CURRENT_TIME").select(OffsetTime.class).findFirst().orElse(null), + instanceOf(OffsetTime.class)); + assertThat(agent.queryWith("select CURRENT_TIMESTAMP").select(LocalDateTime.class).findFirst().orElse(null), + instanceOf(LocalDateTime.class)); + assertThat(agent.queryWith("select CURRENT_TIMESTAMP").select(OffsetDateTime.class).findFirst().orElse(null), + instanceOf(OffsetDateTime.class)); + assertThat(agent.queryWith("select CURRENT_TIMESTAMP").select(ZonedDateTime.class).findFirst().orElse(null), + instanceOf(ZonedDateTime.class)); + assertThat(agent.queryWith("select YEAR(CURRENT_DATE)").select(Year.class).findFirst().orElse(null), + is(Year.now())); + assertThat(agent.queryWith("select 202208").select(YearMonth.class).findFirst().orElse(null), + is(YearMonth.of(2022, 8))); + assertThat(agent.queryWith("select 823").select(MonthDay.class).findFirst().orElse(null), + is(MonthDay.of(8, 23))); + assertThat(agent.queryWith("select MONTH(CURRENT_DATE)").select(Month.class).findFirst().orElse(null), + is(Month.from(LocalDate.now()))); + assertThat( + agent.queryWith("select ISO_DAY_OF_WEEK(CURRENT_DATE)").select(DayOfWeek.class).findFirst() + .orElse(null), + is(DayOfWeek.from(LocalDate.now()))); + + // 配列型 + assertThat(agent.queryWith("select ARRAY[1, 2]").select(Object[].class).findFirst().orElse(null)[1], is(2)); + assertThat(agent.queryWith("select X'616263'").select(byte[].class).findFirst().orElse(null), + is("abc".getBytes())); + + // 例外 + try { + agent.queryWith("select 'abc'").select(null); + assertTrue(false); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), is("Argument 'type' is required.")); + } catch (Exception ex) { + assertTrue(false); + } + + try { + agent.queryWith("select 'abc'").select(Object.class); + assertTrue(false); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), is("java.lang.Object is not supported.")); + } catch (Exception ex) { + assertTrue(false); + } } /**