Sua segunda solução ficou bem melhor do que a primeira, apesar da query ficar dificil de ler, como vc destacou.

De qualquer forma, como vc esta usando Spring Data, pode usar o Query by Example. Vc pode ler mais no link abaixo.

https://docs.spring.io/spring-data/jpa/reference/repositories/query-by-example.html

E este é um exemplo bem básico de como ficaria.

@Getter
@Setter
@Entity
@Table(name = "empresa")
class ParceiroEntity {
  @Id
  @Column(length = 14, nullable = false)
  private String cpf;

  @Column(length = 255, nullable = false)
  private String nome;

  @Column(length = 255, nullable = false)
  private String estado;

  @Column(length = 2, nullable = false)
  private String uf;

  @Column(nullable = false)
  private Integer numeroFuncionarios;

  @Column(nullable = false)
  private Integer qtdFiliais;
}

@Getter
@Setter
class ParceiroRequestParams {
  private String cpf;
  private String nome;
  private String estado;
  private Integer numero;
}

interface ParceiroRepository extends JpaRepository<ParceiroEntity, String> {
}

@RestController
class ParceirosController {
  @Autowired
  private ParceiroRepository repository;

  @GetMapping("/parceiros")
  List<ParceiroEntity> getParceiros(ParceiroRequestParams params) {
    var matcher = ExampleMatcher
      .matching()
      .withStringMatcher(StringMatcher.CONTAINING)
      .withIgnoreNullValues();

    var entity = new ParceiroEntity();
    entity.setCpf(params.getCpf());
    entity.setNome(params.getNome());
    entity.setEstado(params.getEstado());
    entity.setNumeroFuncionarios(params.getNumero());

    var example = Example.of(entity, matcher);

    return repository.findAll(example);
  }
}

A JpaRepository já estende a QueryByExampleExecutor, então não precisamos fazer nenhuma alteração no nosso repository.

Basicamente, como o nome da funcionalidade sugere, a gente cria um exemplo de entidade que queremos trazer do banco.

Funciona bastante bem para vários casos, e é perfeito para este cenário que vc apresentou.

Outra opção é usar as specifications. Veja mais aqui:

https://docs.spring.io/spring-data/jpa/reference/jpa/specifications.html

Apesar de mais complexas, specifications são bastante poderosas!

Obrigado, não conhecia essas opções e gostei muito de usar ExampleMatcher, só tive dificuldade na hora de lidar com valores do tipo int.

Ao usar int os valores foram iniciados automaticamente pelo Java com o valor 0 então minha query acabou ficando assim

Método com Matcher:

public List<ParceiroEntity> findBySearchDynamic(ParceiroRequestParams params){

        ExampleMatcher matcher = ExampleMatcher.matching()
                .withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING)
                .withIgnoreCase()
                .withIgnoreNullValues();

        ParceiroEntity entity = new ParceiroEntity(
                params.getCpf(),
                params.getNome(),
                params.getEstado()
        );

        return repository.findAll(Example.of(entity, matcher));
    }

Query JPA:

select
        pe1_0.cpf,
        pe1_0.estado,
        pe1_0.nome,
        pe1_0.numero_funcionarios,
        pe1_0.qtd_filiais,
        pe1_0.uf 
    from
        empresa pe1_0 
    where
        lower(pe1_0.nome) like ? escape '\' 
        and pe1_0.qtd_filiais=? 
        and pe1_0.numero_funcionarios=?

Neste exemplo na API eu informei como parâmetro apenas o campo nome mas o Java inicializou os dois campos e int e minha query tentou buscar por qtd_filiais = 0 e numero_funcionarios=0 o que fez minha busca retornar nenhum registro.

Bastou trocar o tipo dos campos de int para Integer que agora ele consegue validar campos nulos e funcionou muito bem nos testes que fiz.

Query com os capos qtdFiliaise numeroFuncionarios como Integer:

select
        pe1_0.cpf,
        pe1_0.estado,
        pe1_0.nome,
        pe1_0.numero_funcionarios,
        pe1_0.qtd_filiais,
        pe1_0.uf 
    from
        empresa pe1_0 
    where
      lower(pe1_0.nome) like ? escape '\'

Em resumo o código ficou muito mais conciso, vou estudar melhor para ver as opções que tenho com ExampleMatcher mas se encaixou perfeitamente no que eu precisava.

Maravilha. É um pouco ruim não poder usar tipos primitivos, mas o ganho em simplicidade de código acaba compensando a mudança para as wrapper classes. Eu não tenho certeza, porque não pude testar, mas talvez dê pra contornar esta limitação usando o método `withMatcher`. Na documentação tem um exemplo assim: ```java ExampleMatcher matcher = ExampleMatcher.matching() .withMatcher("firstname", match -> match.endsWith()) .withMatcher("firstname", match -> match.startsWith()); } ``` E eu acredito que nessa expressão lambda, que passamos como segundo argumento, deva dar pra fazer alguma validação envolvendo os tipos primitivos.