Skip to content

JpaSpecificationExecutor.findBy(…) returns join product of nested relations #3908

Closed
@emilianoxhukellari

Description

@emilianoxhukellari

I have the following entities:

@Entity
@Table(name = "application_user")
public class ApplicationUserEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Long id;

    @Column(nullable = false, unique = true)
    private String name;

    @OneToMany(
            mappedBy = "applicationUser", 
            orphanRemoval = true, 
            cascade = CascadeType.ALL, 
            fetch = FetchType.LAZY
    )
    private Set<DeviceEntity> devices = new HashSet<>();

    @OneToOne(
            mappedBy = "applicationUser", 
            cascade = CascadeType.ALL, 
            optional = false
    )
    private SubscriptionEntity subscription;

    // getters and setters omitted for brevity

}
@Entity
@Table(name = "device")
public class DeviceEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Long id;

    @Column(nullable = false)
    private String type;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(nullable = false)
    private ApplicationUserEntity applicationUser;

    // getters and setters omitted for brevity
    
}
@Entity
@Table(name = "subscription")
public class SubscriptionEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Long id;

    @Column(nullable = false)
    private LocalDate expirationDate;

    @OneToOne
    @JoinColumn
    private ApplicationUserEntity applicationUser;

    // getters and setters omitted for brevity

}

In addition, for each entity I have these simple projection interfaces:

public interface ApplicationUserProjection {
    Long getId();
    String getName();
    Set<DeviceProjection> getDevices();
}
public interface DeviceProjection {
    Long getId();
    String getType();
}
public interface SubscriptionProjection {
    Long getId();
    LocalDate getExpirationDate();
    ApplicationUserProjection getApplicationUser();
}

Lastly, I have two simple repositories that extend JpaSpecificationExecutor:

public interface SubscriptionRepository extends JpaRepository<SubscriptionEntity, Long>, JpaSpecificationExecutor<SubscriptionEntity> {
}

public interface ApplicationUserRepository extends JpaRepository<ApplicationUserEntity, Long>, JpaSpecificationExecutor<ApplicationUserEntity> {
}

When I try to project a subscription with its application user and their devices, I expect to get a single subscription with its application user and devices pre-populated to the projection interface:

subscriptionRepository.findBy(
                (root, _, criteriaBuilder) -> criteriaBuilder.equal(root.get("expirationDate"), LocalDate.of(2025, 1, 1)),
                query -> query.as(SubscriptionProjection.class)
                        .project("expirationDate", "applicationUser", "applicationUser.devices")
                        .all()
        );

However I am getting three results with duplicate data except for when I see the debugging information, I see each result has a single device.
This device result, however, is not mapped into the devices collection - when I call subscription.getApplicationUser().getDevices() I get LazyInitializationException since I am outside a transaction.

Both versions 3.4.6 and 3.5.0 project collection attributes correctly when they are direct children. For instance, when we try to project an application user with devices, everything works as expected - we get 1 user with all their devices and no LazyInitializationException:

applicationUserProjections = applicationUserRepository.findBy(
                (root, _, criteriaBuilder) -> criteriaBuilder.equal(root.get("name"), "testUserName"),
                query -> query.as(ApplicationUserProjection.class)
                        .project("name", "devices")
                        .all()
        );

I am attaching a simple test case for you to verify.

fluent-query-collection-projection.zip

Metadata

Metadata

Assignees

Labels

type: regressionA regression from a previous release

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions