Skip to content

Optimize object mapping for value types #129

@zantvoort

Description

@zantvoort

Problem

ObjectMapperFactory.getObjectMapper has no dedicated path for "leaf value types" like UUID, String, BigDecimal, boxed primitives, and java.time.*. When a query maps to one of these as a scalar (e.g. query.getSingleResult(UUID.class)), the factory falls through every special case and lands in the constructor-scan loop:

  for (Constructor<?> constructor : type.getDeclaredConstructors()) {
      if (constructor.getParameterTypes().length == columnCount) {
          return Optional.of(wrapConstructor(constructor));
      }
  }

It then calls setAccessible(true) on the first matching constructor. For UUID.class with columnCount == 1, the only 1-arg constructor is the private UUID(byte[]) in java.base, and the JDK module system refuses access:

  java.lang.reflect.InaccessibleObjectException: Unable to make private java.util.UUID(byte[]) accessible:
  module java.base does not "opens java.util" to unnamed module @685f4c2e
      at st.orm.core.template.impl.ObjectMapperFactory.construct(ObjectMapperFactory.java:172)

For other leaf types the same path "works" only by luck of getDeclaredConstructors() ordering — e.g. String.class happens to land on the public String(String) copy-constructor, and we pay one allocation per row to wrap a value that's already correct. For BigDecimal.class and others, behavior depends on whichever 1-arg constructor the JVM returns first.

Affected APIs

Any call that maps a single column to a leaf type, including:

  • Query.getSingleResult(UUID.class) / getResultStream(UUID.class)
  • Query.getRefStream(EntityType.class, UUID.class) (PK type lookup)
  • Single-column projections / aggregates with these target types

Proposed fix

Add a ValueMapper next to PrimitiveMapper / EnumMapper that provides a single-column pass-through for types QueryImpl.readColumnValue already returns directly:

  • Boxed primitives: Boolean, Byte, Short, Integer, Long, Float, Double
  • String, BigDecimal, ByteBuffer, UUID
  • java.util.Date, Calendar, java.sql.Date, Time, Timestamp
  • LocalDateTime, LocalDate, LocalTime, Instant, OffsetDateTime, ZonedDateTime

Dispatch to ValueMapper.getFactory(columnCount, type) in ObjectMapperFactory.getObjectMapper before the constructor-scan fallback. The mapper just returns args[0] — no reflection, no allocation, no module-access issues.

Benefits

  • Fixes InaccessibleObjectException for UUID and any other JDK type whose 1-arg constructors are private.
  • Removes brittle reliance on getDeclaredConstructors() ordering for String, BigDecimal, etc.
  • Eliminates one allocation per row for String.class queries (no more new String(s)).
  • Aligns scalar value mapping with what readColumnValue already produces.

Test coverage

Add regression tests under EntityRepositoryIntegrationTest (or a new dedicated test) for:

  • getSingleResult(UUID.class) on a single UUID column.
  • getResultStream(UUID.class) on a multi-row UUID column.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions