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

Return value of Repository.save contains only added elements of @MappedCollection, not existing ones #1907

Closed
chris-unlikelyai opened this issue Oct 4, 2024 · 3 comments
Assignees
Labels
type: bug A general bug

Comments

@chris-unlikelyai
Copy link

I have two entities, one containing a List of the other, mapped to two tables using @MappedCollection. There's a repository for the parent:

@Table
data class ParentEntity(
  @MappedCollection val children: List<ChildEntity>,
  @Id val id: UUID? = null,
)

@Table
data class ChildEntity(@Id val id: UUID? = null)

interface EntityRepository : CrudRepository<ParentEntity, UUID>

If I have an existing entity in the database with a single child entity, loading it, adding a second child entity, and saving does the right thing in the database, but the parent entity returned by the EntityRepository.save method returns a list containing only the new child, not the original child plus the new child. Loading from the DB again returns the correct result.

Full code to reproduce below:

package springdataissue

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.boot.runApplication
import org.springframework.context.event.EventListener
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.MappedCollection
import org.springframework.data.relational.core.mapping.Table
import org.springframework.data.repository.CrudRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import java.util.UUID

// Schema:
//
// CREATE TABLE IF NOT EXISTS parent_entity
// (
//     id                 UUID NOT NULL DEFAULT random_uuid() PRIMARY KEY
// );
//
// CREATE TABLE IF NOT EXISTS child_entity
// (
//     id                 UUID NOT NULL DEFAULT random_uuid() PRIMARY KEY,
//     parent_entity      UUID NOT NULL REFERENCES parent_entity (id) ON DELETE CASCADE,
//     parent_entity_key  INT  NOT NULL
// );

@Table
data class ParentEntity(
  @MappedCollection val children: List<ChildEntity>,
  @Id val id: UUID? = null,
)

@Table
data class ChildEntity(@Id val id: UUID? = null)

interface EntityRepository : CrudRepository<ParentEntity, UUID>

@SpringBootApplication class SpringDataIssue

@Service
class Startup(private val entityRepository: EntityRepository) {
  @EventListener(ApplicationReadyEvent::class)
  fun onStartup() {
    val entity1 = entityRepository.save(ParentEntity(listOf(ChildEntity())))
    println("After initial save:            $entity1")

    val entity2 = entity1.copy(children = entity1.children + ChildEntity())
    println("Updated in memory:             $entity2")

    val entity3 = entityRepository.save(entity2)
    println("After saving updated entity:   $entity3")

    val entityE = entityRepository.findByIdOrNull(entity3.id!!)!!
    println("Updated entity loaded from DB: $entityE")
  }
}

fun main(args: Array<String>) {
  runApplication<SpringDataIssue>(*args)
}

// Result:
//   After initial save:            ParentEntity(children=[ChildEntity(id=542938e4-1941-40bb-852b-6b11052c5a3b)], id=c93a8ba8-0c54-4979-af25-d08f422803d7)
//   Updated in memory:             ParentEntity(children=[ChildEntity(id=542938e4-1941-40bb-852b-6b11052c5a3b), ChildEntity(id=null)], id=c93a8ba8-0c54-4979-af25-d08f422803d7)
//   After saving updated entity:   ParentEntity(children=[ChildEntity(id=a9686057-a17d-4626-b649-ab512155bfd3)], id=c93a8ba8-0c54-4979-af25-d08f422803d7)
//   Updated entity loaded from DB: ParentEntity(children=[ChildEntity(id=542938e4-1941-40bb-852b-6b11052c5a3b), ChildEntity(id=a9686057-a17d-4626-b649-ab512155bfd3)], id=c93a8ba8-0c54-4979-af25-d08f422803d7)
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Oct 4, 2024
@schauder
Copy link
Contributor

schauder commented Oct 4, 2024

Please provide a Minimimal Reproducable Example, preferable as a Github repository. Make sure to include the database, either as an in memory database or if that is not possible using Testcontainers.

@schauder schauder added status: waiting-for-feedback We need additional information before we can continue and removed status: waiting-for-triage An issue we've not yet triaged labels Oct 4, 2024
@chris-unlikelyai
Copy link
Author

Here's a reproduceable example as a GitHub repo:

https://github.com/chris-unlikelyai/springdataissue

Clone and run mvn spring-boot:run to reproduce.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Oct 4, 2024
@schauder
Copy link
Contributor

Analysis:

For immutable entities we gather changed entities (ChildEntity), in order to set them as values in entities (ParentEntity) referencing them. But we don't do this for unchanged (ChildEntity).

In the case of a collection of entities this leads to the effect you see, because we gather only those ChildEntity instances that actually got a new id created.

@schauder schauder self-assigned this Oct 18, 2024
@schauder schauder added the type: bug A general bug label Oct 18, 2024
schauder added a commit that referenced this issue Oct 23, 2024
We gather immutable entities of which the id has changed, in order to set them as values in the parent entity.
We now also gather unchanged entities.
So they get set with the changed one in the parent.

Closes #1907
@mp911de mp911de changed the title Return value of Repository.save contains only added elements of @MappedCollection List, not existing ones Return value of Repository.save contains only added elements of @MappedCollection, not existing ones Oct 25, 2024
@mp911de mp911de removed the status: feedback-provided Feedback has been provided label Oct 25, 2024
@mp911de mp911de added this to the 3.2.12 (2023.1.12) milestone Oct 25, 2024
mp911de added a commit that referenced this issue Oct 25, 2024
Eliminate potential NoSuchElementException from unchecked Optional.get usage. Simplify stream. Return Staged value, fix Nullability annotations.

See #1907
Original pull request: #1920
mp911de pushed a commit that referenced this issue Oct 25, 2024
We gather immutable entities of which the id has changed, in order to set them as values in the parent entity.
We now also gather unchanged entities.
So they get set with the changed one in the parent.

Closes #1907
Original pull request: #1920
mp911de added a commit that referenced this issue Oct 25, 2024
Eliminate potential NoSuchElementException from unchecked Optional.get usage. Simplify stream. Return Staged value, fix Nullability annotations.

See #1907
Original pull request: #1920
mp911de pushed a commit that referenced this issue Oct 25, 2024
We gather immutable entities of which the id has changed, in order to set them as values in the parent entity.
We now also gather unchanged entities.
So they get set with the changed one in the parent.

Closes #1907
Original pull request: #1920
mp911de added a commit that referenced this issue Oct 25, 2024
Eliminate potential NoSuchElementException from unchecked Optional.get usage. Simplify stream. Return Staged value, fix Nullability annotations.

See #1907
Original pull request: #1920
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug A general bug
Projects
None yet
4 participants