JpaSpecification 使用泛型适用于字符串,但日期和连接字段有问题

问题描述

我正在尝试扩展这个 Baeldung 教程 https://www.baeldung.com/rest-api-search-language-spring-data-specifications

但我希望规范是通用的,并且我希望允许客户端按嵌入对象的值进行搜索。一切都适用于字符串和一些数字,但不适用于 id 和其他更复杂的对象,如 Date

我的模型:(假设一个人只能有 1 只宠物)

@Entity
public Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private ID id;
    private String name;
    private Date dateOfBirth
    private Integer age;
    private Pet pet;
    // Getter & Setters etc
}

@Entity
public Pet {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private ID id;
    private String type;
    private String name;
    private Integer numOfLegs;
    // Getter & Setters etc
}

个人仓库:

@Repository
public interface PersonRepository extends JpaRepository<Person,Integer>,JpaSpecificationExecutor<Person>{}

将保存我们可以搜索的键、运算符和值的搜索条件。

public class EntitySearchCriteria {
    private String key;
    private String operation;
    private Object value;
    
    public EntitySearchCriteria(final String key,final String operation,final Object value) {
        this.key = key;
        this.operation = operation;
        this.value = value;
    }  
    // Getters and Setters etc

我的通用规范类(这实际上是构建要使用的谓词的地方)。这也允许客户端在连接表的值上设置 SearchCriteria。例如“宠物名称=松饼”

public abstract class AbstractEntitySpecification<T,ID extends Serializable> implements Specification<T> {

    protected EntitySearchCriteria criteria;

    @Override
    public Predicate toPredicate(Root<T> root,CriteriaQuery<?> query,CriteriaBuilder criteriaBuilder) {
        if (criteria.getoperation().equalsIgnoreCase(">")) {
            return criteriaBuilder.greaterThanorEqualTo(root.<String>get(criteria.getKey()),criteria.getValue().toString());
        } else if (criteria.getoperation().equalsIgnoreCase("<")) {
            return criteriaBuilder.lessthanorEqualTo(root.<String>get(criteria.getKey()),criteria.getValue().toString());
        } else if (criteria.getoperation().equalsIgnoreCase(":")) {
            if (criteria.getKey().contains(".")) {
                String[] joinCriteriaArray = criteria.getKey().split("\\.");
                Class<?> joinedClass = root.get(joinCriteriaArray[0]).getClass();
                Join<T,?> joinedRelationship = root.join(joinCriteriaArray[0]);
                return criteriaBuilder.equal(joinedRelationship.get(joinCriteriaArray[1]),criteria.getValue());
            }
            if (root.get(criteria.getKey()).getJavaType() == String.class) {
                return criteriaBuilder.like(root.<String>get(criteria.getKey()),"%" + criteria.getValue() + "%");
            } else {
                return criteriaBuilder.equal(root.get(criteria.getKey()),criteria.getValue());
            }
        }
        return null;
    }
}

我想要允许这种类型的查询的任何实体只需要具体实现 AbstractEntitySpecification

public class PersonSpecification extends AbstractEntitySpecification<Person,Integer> {
    public PersonSpecification (final EntitySearchCriteria entitySearchCriteria) {
        this.criteria = entitySearchCriteria;
    }
}

这些是我运行的测试。对字符串或 Int 的 Person 属性(即 Person.name、Person.age)的任何搜索都有效,但对 dateOfBirth 的搜索无效。 任何对作为字符串的宠物的属性搜索都可以使用连接进行,但对 id(Integer) 的搜索不会,无论我是将 id 作为 Int 还是 String 传递。我已将行为放在每个测试的评论中。

public class PersonSpecificationMediumTest extends AbstractMediumTest {
    @Autowired
    private PersonRepository personRepository;

    @Autowired
    private PetRepository petRepository;
    
    Person person1;
    Person person2;

    @Before
    public void setUp() {
        Pet muffins = new Pet(1,"cat","muffins",4);
        Pet rex= new Pet(2,"dog","rex",4);
        petRepository.saveAll(Arrays.asList(muffins,rex));

        person1 = new Person();
        person1.setName("David");
        person1.setDateOfBirth(Date.parse("1979-03-01");
        person1.setPet(muffins);
        person1 = personRepository.saveAndFlush(person1);

        person2 = new Person();
        person2.setName("Mary");
        person2.setDateOfBirth(Date.parse("1982-03-01");
        person2.setPet(rex);
        person2 = personRepository.saveAndFlush(person2);
    }

    @Test //Works
    public void toPredicate_findByNameEquals_assertCorrectResult() {
        PersonSpecification spec
                = new PersonSpecification(new EntitySearchCriteria("name",":","David"));
        
        List<Person> results = personRepository.findAll(spec);
        
        Assert.assertEquals(person1,results.get(0));
    }

    @Test // Works
    public void toPredicate_findByPetNameEquals_assertCorrectResult() {
       PersonSpecification spec
                = new PersonSpecification(new EntitySearchCriteria("client.name","Rex"));
        
        List<Person> results = personRepository.findAll(spec);
        
         Assert.assertEquals(person2,results.get(0));
    }

    @Test // Return empty list. Cannot find the pet by Id.
    public void toPredicate_findByPetIdEquals_assertCorrectResult() {
         PersonSpecification spec
                = new PersonSpecification(new EntitySearchCriteria("pet.id",2));
        
         List<Person> results = personRepository.findAll(spec);
        
         Assert.assertEquals(person2,results.get(0));
    }

    @Test // org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [2] did not match expected type [java.lang.Integer (n/a)];
    public void toPredicate_findByPetIdAsstringEquals_assertCorrectResult() {
         PersonSpecification spec
                = new PersonSpecification(new EntitySearchCriteria("pet.id","2"));

         List<Person> results = personRepository.findAll(spec);

         Assert.assertEquals(person2,results.get(0));
    }

    @Test // Fails on org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [2020-01-01] did not match expected type [java.util.Date (n/a)]
    public void toPredicate_findByDateOfBirthBetween_assertCorrectResult() {
        PersonSpecification spec1
                = new PersonSpecification(new EntitySearchCriteria("dateOfBirth","<","1990-01-01"));
        PersonSpecification spec2
                = new PersonSpecification(new EntitySearchCriteria("dateOfBirth",">","1970-01-01"));
        
        List<Person> results = personRepository.findAll(spec1.and(spec2));
        
        Assert.assertTrue(results.size() == 2);
    }
}

知道为什么 Date 如此成问题吗?我想在 greaterThanorEqualTolessthanorEqualTo 中使用日期,但是传入 criteria.getValue(Object) 会产生编译错误,因此它迫使我使用对象的字符串表示形式。但是显示错误org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [2020-01-01] did not match expected type [java.util.Date (n/a)],它向我表明它无法将 String 与 Date 进行比较,这是有道理的,但为什么要阻止我传递 Date 对象?

另外,为什么 Id 在连接表上会出现这样的问题?为什么它找不到id = 2,我还以为会直截了当,尤其是我可以通过Pets的腿数成功搜索。它必须与 id 可序列化有关。

解决方法

查看JavaDoc for Date.parse。基本部分已经包含声明:

@已弃用
public static long parse(String s)

正如明确说明的那样,它返回一个 long 值。要获取 Date 对象,您可以使用 SimpleDateFormat that inherits DateFormat.parse(String s),例如:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date d1 = sdf.parse("1979-03-01");