简体   繁体   English

使用带有可为空外键的 QueryDSL 在 Spring Data JPA Repository 中进行过滤

[英]Filtering in Spring Data JPA Repository using QueryDSL with a nullable foreign key

I've been having this issue where I am unable to properly filter on a table using querydsl which has a nullable foreign key.我一直遇到这个问题,我无法使用具有可为空外键的 querydsl 正确过滤表。 I stripped down my use case into a very simple scenario.我将我的用例简化为一个非常简单的场景。

Say we have 2 entities, MyEntity and TimeRangeEntity.假设我们有 2 个实体,MyEntity 和 TimeRangeEntity。 My Entity only has an ID and a foreign key to the TimeRangeEntity.我的实体只有一个 ID 和一个指向 TimeRangeEntity 的外键。 The TimeRangeEntity only has a start and an end time and an ID. TimeRangeEntity 只有开始时间和结束时间以及 ID。 BaseEntity, that these both extend from, only has the ID set with the @Id annotation. BaseEntity,它们都扩展自,只有带有 @Id 注释的 ID 集。

@Entity
@Table(name = "TEST_OBJECT")
public class MyEntity extends BaseEntity {
    @OneToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST })
    private TimeRangeEntity actionTime;

    public TimeRangeEntity getActionTime() {
        return actionTime;
    }

    public void setActionTime(TimeRangeEntity actionTime) {
        this.actionTime = actionTime;
    }
}


@Entity
@DiscriminatorValue("static")
public class TimeRangeEntity extends BaseEntity {

    @Column(name = "START_TIME")
    private Instant startTime;

    @Column(name = "END_TIME")
    private Instant endTime;

    public Instant getStartTime() {
        return startTime;
    }

    public void setStartTime(Instant startTime) {
        this.startTime = startTime;
    }

    public Instant getEndTime() {
        return endTime;
    }

    public void setEndTime(Instant endTime) {
        this.endTime = endTime;
    }
}

I've constructed a default method in my repository to run a findAll with a predicate using querydsl to build the SQL syntax我在我的存储库中构建了一个默认方法来运行带有谓词的 findAll 使用 querydsl 来构建 SQL 语法

@Repository
public interface MyRepository extends JpaRepository<MyEntity, Long>, QueryDslPredicateExecutor<MyEntity> {

    default Page<MyEntity> paginateFilter(PaginationInfo info, String filter){
        int page = info.getOffset() > 0 ? info.getOffset() / info.getLimit() : 0;
        PageRequest pageRequest = new PageRequest(page, info.getLimit(), new Sort(new Sort.Order(info.getSortDirection(), info.getSortProperty())));
        return findAll(createFilterPredicate(filter, myEntity), pageRequest);
    }

    default Predicate createFilterPredicate(String filter, QMyEntity root){
        BooleanBuilder filterBuilder = new BooleanBuilder();
        filterBuilder.or(root.id.stringValue().containsIgnoreCase(filter));
        filterBuilder.or(root.actionTime.startTime.isNotNull());
        return filterBuilder.getValue();
    }
}

I also wrote a test that should work given the code presented.我还编写了一个测试,在给出的代码下应该可以工作。 What I'm trying to do is just filter based on ID.我想要做的只是根据 ID 进行过滤。 The caveat is that the FK to the TimeRange can be null.需要注意的是,TimeRange 的 FK 可以为空。 I'll note that this a contrived example to get my point across and the solution can't really be "just enforce the FK is not null."我会注意到这是一个人为的例子来表达我的观点,解决方案不能真的是“只是强制 FK 不为空”。

@RunWith(SpringRunner.class)
@DataJpaTest(showSql = false)
@ContextConfiguration(classes = TestConfig.class)
@ActiveProfiles("test")
public class MyRepositoryTest {
    @Autowired
    private MyRepository sut;

    private static final int count = 3;

    @Before
    public void setup(){
        for (int i = 0; i < count; i++){
            sut.save(new MyEntity());
        }
    }

    @Test
    public void testPaginationWithStringFilter(){
        PaginationInfo info = new PaginationInfo();
        info.setOffset(0);
        info.setLimit(10);
        info.setSortDirection(Sort.Direction.ASC);
        info.setSortProperty("id");

        Page<MyEntity> page = sut.paginateFilter(info, "1");
        assertEquals(1, page.getTotalElements());

        page = sut.paginateFilter(info, "10");
        assertEquals(0, page.getTotalElements());
    }
}

The problem that I'm running into is that it isn't filtering on the ID if the FK is null.我遇到的问题是,如果 FK 为空,它不会对 ID 进行过滤。 All I'm doing when I save is setting the ID.我保存时所做的就是设置 ID。 I know the problem is because I can see the filtering work properly when I comment out the line filterBuilder.or(root.actionTime.startTime.isNotNull());我知道问题是因为当我注释掉filterBuilder.or(root.actionTime.startTime.isNotNull());我可以看到过滤正常工作filterBuilder.or(root.actionTime.startTime.isNotNull()); but it doesn't work when I have that in.但是当我把它放进去时它不起作用。

This generates the following queries.这会生成以下查询。 The first is for the "working" filtering where I can filter based on ID (line commented out).第一个是“工作”过滤,我可以根据 ID(注释掉的行)进行过滤。 The second is for the filtering with the actionTime included.第二个是用于包含 actionTime 的过滤。

select myentity0_.id as id2_38_, myentity0_.action_time_id as action_t3_38_ from test_object myentity0_ where lower(cast(myentity0_.id as char)) like ? escape '!' order by myentity0_.id asc limit ?

select myentity0_.id as id2_38_, myentity0_.action_time_id as action_t3_38_ from test_object myentity0_ cross join time_range_entity timerangee1_ where myentity0_.action_time_id=timerangee1_.id and (lower(cast(myentity0_.id as char)) like ? escape '!' or timerangee1_.start_time is not null) order by myentity0_.id asc limit ?

Looking at this, I'm almost certain that this is due to the snipper cross join time_range_entity timerangee1_ where myentity0_.action_time_id=timerangee1_.id since it validates that the entities match, which they cannot if the range foreign key is null.看着这个,我几乎可以肯定这是由于 snipper cross join time_range_entity timerangee1_ where myentity0_.action_time_id=timerangee1_.id因为它验证实体匹配,如果范围外键为空,则它们不能匹配。

I've been pulling my hair out trying to get this conditional working that only checks the time range's table properties IF the FK is not null but I cannot find a way using querydsl.我一直在努力让这个有条件的工作只检查时间范围的表属性,如果 FK 不为空,但我找不到使用 querydsl 的方法。 Any advice/guidance/code snippets would be stellar.任何建议/指导/代码片段都会很棒。

EDIT : Just translating to straight SQL, I got this query for the generated JPQL(translated to this example since I used it with real data):编辑:只是转换为直接的 SQL,我得到了生成的 JPQL 的这个查询(转换到这个例子,因为我使用它与真实数据):

select * from test_object cross join time_range range where test_object.action_time_id=range.id and lower(cast(test_object.id as char)) like '%1%';

With a null FK, that didn't return a row as expected.使用空 FK,它没有按预期返回一行。 Changing this to a left join from a cross join ended up working properly.将其从交叉连接更改为左连接最终正常工作。

select * from test_object left join time_range on test_object.action_time_id=time_range.id where lower(cast(test_object.id as char)) like '%1%';

With that, is there any way to specify a left join with the querydsl predicate executor?有了这个,有没有办法指定与 querydsl 谓词执行器的左连接? This seems like it'd be the solution to my problem!这似乎是我问题的解决方案!

Try to use Specification instead of Predicate尝试使用规范而不是谓词

  private Specification<QMyEntity> createFilterPredicate(final String filter, final QMyEntity root) {
        return new Specification<QMyEntity>() {
            @Nullable
            @Override
            public Predicate toPredicate(Root<QMyEntity> root, CriteriaQuery<?> query,
                    CriteriaBuilder criteriaBuilder) {
                Join<Object, Object> actionTime = root.join("actionTime", JoinType.LEFT);
                return criteriaBuilder.or(criteriaBuilder.like(criteriaBuilder.lower(root.get("id")), "%" + filter + "%"), criteriaBuilder.isNotNull(actionTime.get("startTime")));
            }
        };
    }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM