Description
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.