JPA一对多关联如何筛选特定子实体?自定义查询未生效问题
我基于JPA定义了宠物店的实体模型,包含抽象类Pet、实体PetShop,以及自定义的PetShopRepository查询方法。创建并保存了包含1只存活宠物和1只死亡宠物的宠物店后,调用自定义的findById方法,期望返回的PetShop实例仅包含存活宠物,但实际返回了全部2只宠物,请问问题出在哪?
模型代码
Pet类
@Entity @Table(name = "pet") @Getter @Setter @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode public abstract class Pet { @Column(name = "id", nullable = false) private Long id; @Column(name = "name", nullable = false) private String name; @Column(name = "birth_date", nullable = false) private LocalDate birthDate; @Column(name = "death_date") private LocalDate deathDate; @ManyToOne @JoinColumn(name = "pet_shop_id", nullable = false, referencedColumnName = "id") @Setter(AccessLevel.NONE) private PetShop petShop; public void setPetShop(PetShop petShop) { setPetShop(petShop, true); } public void setPetShop(PetShop petShop, boolean add) { this.petShop= petShop; if (petShop!= null && add) { petShop.addPet(this, false); } }
PetShop类
@Entity @Table(name = "pet_shop") @Getter @Setter @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode public class PetShop { @Column(name = "id", nullable = false) private Long id; ... @OneToMany( mappedBy = "petShop", fetch = FetchType.LAZY, cascade = {CascadeType.ALL}) private List<Pet> pets= new ArrayList<>(); public void addPet(final Pet pet) { addPet(pet, true); } public void addPet(final Pet pet, boolean set) { if (pet!= null) { if (pets.contains(pet)) { pets.set(pets.indexOf(pet), pet); } else { pets.add(pet); } if (set) { pet.setPetShop(this, false); } } } }
PetShopRepository接口
public interface PetShopRepository extends JpaRepository<PetShop, Long> { @Query( "SELECT DISTINCT ps FROM PetShop ps" + " JOIN ps.pets p" + " WHERE ps.id = :id AND p.deathDate IS NULL") @Override Optional<PetShop> findById(@NonNull Long id); }
测试代码
保存数据
final Pet alive = new Pet(); alive.setName("cat"); alive.setCall("meow"); alive.setBirthDate(LocalDate.now()); final Pet dead = new Pet(); dead.setName("cat"); dead.setCall("meow"); dead.setBirthDate(LocalDate.now().minusYears(15L)); dead.setDeathDate(LocalDate.now()); final PetShop petShop = new PetShop(); petShop.getPets().add(alive); petShop.getPets().add(dead); petShopRepositiry.save(petShop);
查询数据
final PetShop petShop = petShopRepository.findById(shopId) .orElseThrow(() -> new ShopNotFoundException(shopId)); final int petCount = petShop.getPets().size(); // 预期为1,实际为2
核心原因
你的自定义查询仅完成了筛选关联存活宠物的PetShop这一步,但并没有修改PetShop实体中pets集合的加载逻辑。JPA默认会根据映射关系(mappedBy="petShop")加载该宠物店下所有关联的Pet数据,查询中的JOIN ps.pets p WHERE p.deathDate IS NULL只是用来判断哪些PetShop符合查询条件,不会自动裁剪该PetShop的pets集合内容。
解决办法
方法1:使用FETCH JOIN过滤关联集合
修改查询语句,用FETCH JOIN明确指定加载符合条件的宠物,JPA会将过滤后的宠物填充到PetShop的pets集合中:
@Query( "SELECT DISTINCT ps FROM PetShop ps" + " JOIN FETCH ps.pets p" + " WHERE ps.id = :id AND p.deathDate IS NULL") @Override Optional<PetShop> findById(@NonNull Long id);
注意:DISTINCT是必须的,因为JOIN操作会导致PetShop实例被重复返回,需要通过去重得到唯一的PetShop对象。
方法2:用@Where注解全局过滤集合
如果希望所有加载PetShop的场景都只返回存活宠物,可以在PetShop的pets集合上添加@Where注解:
@OneToMany( mappedBy = "petShop", fetch = FetchType.LAZY, cascade = {CascadeType.ALL}) @Where(clause = "death_date IS NULL") private List<Pet> pets= new ArrayList<>();
这种方式是全局生效的,所有查询PetShop时,pets集合都会自动过滤死亡宠物。如果需要偶尔查询包含死亡宠物的场景,可以单独定义一个不带过滤的集合,或者编写额外的查询方法。
方法3:单独查询存活宠物
如果不需要将存活宠物绑定到PetShop实体中,可以单独编写查询方法获取指定宠物店下的存活宠物:
@Query("SELECT p FROM Pet p WHERE p.petShop.id = :shopId AND p.deathDate IS NULL") List<Pet> findAlivePetsByShopId(Long shopId);
这种方式更灵活,不会影响PetShop实体本身的关联集合逻辑。
内容的提问来源于stack exchange,提问作者j3d

