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

Result mapping of projections should always use the DTO and not the domain entity #1554

Closed
RobertHeim opened this issue Jun 30, 2023 · 1 comment
Labels
type: enhancement A general enhancement

Comments

@RobertHeim
Copy link
Contributor

TL;DR: Could the result mapping strategy for DTO projections be changed to always use the DTO and not the domain entity? That would save us a lot of error prone and repetitive work, because in Kotlin null-safety prohibits instantiating the domain entities first, if not all non-null fields are also selected in the query.

Details

From the docs I get that result mapping of DTO projections basically consists of these two facts:

  • select only the columns that the DTO defines as attributes
  • instantiate the domain entity and then map the domain entity to the DTO.

Effectively this means that the instantiated domain entity sets all fields null that were not queried. This works in Java, but not in Kotlin, because of null-safety.

For example:

data class Avatar(
  @Id
  val id: UUID? = null,
  val userId: Long,
  val avatar: String
)

data class AvatarIdWithUserId(
  val id: UUID,
  val userId: Long,
)

@Repository
interface AvatarRepository : CoroutineCrudRepository<Avatar, UUID> {
  fun findByUserIdIn(ids: Set<Long>): Flow<AvatarIdWithUserId>
}

If there is an avatar for userId 1L the query findByUserIdIn(setOf(1L)) fails with

Caused by: java.lang.NullPointerException: Parameter specified as non-null is null: method my.package.Avatar.<init>, parameter avatar

From the docs I understand that to make that work it would require me to write a @Query annotation, because then the result mapping uses the DTO immediately:

  @Query("SELECT id, user_id FROM avatar WHERE user_id IN (:ids)")
  fun findByUserIdIn(ids: Set<Long>): Flow<AvatarIdWithUserId>

Obviously, that is error prone and unnecessary work. For instance, a major issue in this example is that the query would fail if ids is empty. Hence, the caller side must first guarantee that ids is non empty - which is cumbersome. Hence, it would require us to provide a custom implementation for that simple query to return if ids is empty.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jun 30, 2023
@mp911de
Copy link
Member

mp911de commented Jul 3, 2023

When Spring Data introduced projections, we used the underlying domain entity as the backing data structure and then copy over matching properties into the DTO (or apply an interface projection proxy). For some Spring Data modules (MongoDB, Cassandra), we started migrating.

The main reasons for that approach have been that:

  1. we needed a backing data structure
  2. domain type mapping rules must be applied
  3. potential type conversion must be applied

In Mongo and Cassandra modules we introduced multi-level projections (EntityProjection) and with that we use a Map as storage for projection proxies. For DTO's we read projected properties directly into the DTO.

I think we can apply a similar approach in JDBC and R2DBC modules, hence moving this ticket into Spring Data Relational.

@mp911de mp911de transferred this issue from spring-projects/spring-data-r2dbc Jul 3, 2023
@mp911de mp911de added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Jul 3, 2023
@mp911de mp911de changed the title result mapping of projections should always use the DTO and not the domain entity Result mapping of projections should always use the DTO and not the domain entity Sep 18, 2023
schauder pushed a commit that referenced this issue Oct 13, 2023
schauder pushed a commit that referenced this issue Oct 13, 2023
schauder pushed a commit that referenced this issue Oct 13, 2023
Merge basic converters into Mapping…Converters and introduce deprecated variant to provide guidance for migration off the deprecated types.

Cleanup no longer required code.

Original pull request #1618
See #1554
schauder pushed a commit that referenced this issue Oct 13, 2023
schauder added a commit that referenced this issue Oct 13, 2023
Original pull request #1618
See #1554
@schauder schauder added this to the 3.2 RC1 (2023.1.0) milestone Oct 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants