REST-Abfragesprache mit Spring Data JPA und Querydsl

REST-Abfragesprache mit Spring-Daten JPA und Querydsl

1. Überblick

In diesem Tutorial möchten wir eine Abfragesprache fürREST API using Spring Data JPA and Querydsl erstellen.

In den ersten beiden Artikeln vonthis series haben wir dieselbe Such- / Filterfunktion unter Verwendung der JPA-Kriterien und der Spring Data JPA-Spezifikationen erstellt.

Also -why a query language? Weil - für eine ausreichend komplexe API - das Suchen / Filtern Ihrer Ressourcen nach sehr einfachen Feldern einfach nicht ausreicht. A query language is more flexible und ermöglicht es Ihnen, auf genau die Ressourcen zu filtern, die Sie benötigen.

2. Querydsl-Konfiguration

Lassen Sie uns zunächst sehen, wie Sie unser Projekt für die Verwendung von Querydsl konfigurieren.

Wir müssenpom.xml die folgenden Abhängigkeiten hinzufügen:


    com.querydsl
    querydsl-apt
    4.1.4
    

    com.querydsl
    querydsl-jpa
    4.1.4

Wir müssen auch das APT - Annotation Processing Tool - Plugin wie folgt konfigurieren:


    com.mysema.maven
    apt-maven-plugin
    1.1.3
    
        
            
                process
            
            
                target/generated-sources/java
                com.mysema.query.apt.jpa.JPAAnnotationProcessor
            
        
    

3. DieMyUser-Entität

Als nächstes werfen wir einen Blick auf die Entität "MyUser", die wir in unserer Such-API verwenden werden:

@Entity
public class MyUser {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String firstName;
    private String lastName;
    private String email;

    private int age;
}

4. BenutzerdefiniertePredicate mitPathBuilder

Jetzt erstellen wir ein benutzerdefiniertesPredicate basierend auf einigen willkürlichen Einschränkungen.

Wir verwenden hierPathBuilder anstelle der automatisch generierten Q-Typen, da wir für eine abstraktere Verwendung Pfade dynamisch erstellen müssen:

public class MyUserPredicate {

    private SearchCriteria criteria;

    public BooleanExpression getPredicate() {
        PathBuilder entityPath = new PathBuilder<>(MyUser.class, "user");

        if (isNumeric(criteria.getValue().toString())) {
            NumberPath path = entityPath.getNumber(criteria.getKey(), Integer.class);
            int value = Integer.parseInt(criteria.getValue().toString());
            switch (criteria.getOperation()) {
                case ":":
                    return path.eq(value);
                case ">":
                    return path.goe(value);
                case "<":
                    return path.loe(value);
            }
        }
        else {
            StringPath path = entityPath.getString(criteria.getKey());
            if (criteria.getOperation().equalsIgnoreCase(":")) {
                return path.containsIgnoreCase(criteria.getValue().toString());
            }
        }
        return null;
    }
}

Beachten Sie, wie die Implementierung des Prädikatsgenerically dealing with multiple types of operations ist. Dies liegt daran, dass die Abfragesprache per Definition eine offene Sprache ist, in der Sie mithilfe einer unterstützten Operation potenziell nach einem beliebigen Feld filtern können.

Um diese Art von offenen Filterkriterien darzustellen, verwenden wir eine einfache, aber recht flexible Implementierung -SearchCriteria:

public class SearchCriteria {
    private String key;
    private String operation;
    private Object value;
}

DasSearchCriteria enthält die Details, die wir benötigen, um eine Einschränkung darzustellen:

  • key: der Feldname - zum Beispiel:firstName,age, ... usw.

  • operation: die Operation - zum Beispiel: Gleichheit, kleiner als, ... usw.

  • value: der Feldwert - zum Beispiel: John, 25, ... usw.

5. MyUserRepository

Schauen wir uns jetzt unsereMyUserRepositoryan.

Wir brauchen unsereMyUserRepository, umQuerydslPredicateExecutor zu erweitern, damit wir späterPredicates verwenden können, um Suchergebnisse zu filtern:

public interface MyUserRepository extends JpaRepository,
  QuerydslPredicateExecutor, QuerydslBinderCustomizer {
    @Override
    default public void customize(
      QuerydslBindings bindings, QMyUser root) {
        bindings.bind(String.class)
          .first((SingleValueBinding) StringExpression::containsIgnoreCase);
        bindings.excluding(root.email);
      }
}

6. KombinierePredicates

Als Nächstes werfen wir einen Blick auf das Kombinieren von Prädikaten, um mehrere Einschränkungen bei der Ergebnisfilterung zu verwenden.

Im folgenden Beispiel arbeiten wir mit einem Builder -MyUserPredicatesBuilder -, umPredicates zu kombinieren:

public class MyUserPredicatesBuilder {
    private List params;

    public MyUserPredicatesBuilder() {
        params = new ArrayList<>();
    }

    public MyUserPredicatesBuilder with(
      String key, String operation, Object value) {

        params.add(new SearchCriteria(key, operation, value));
        return this;
    }

    public BooleanExpression build() {
        if (params.size() == 0) {
            return null;
        }

        List predicates = params.stream().map(param -> {
            MyUserPredicate predicate = new MyUserPredicate(param);
            return predicate.getPredicate();
        }).filter(Objects::nonNull).collect(Collectors.toList());

        BooleanExpression result = Expressions.asBoolean(true).isTrue();
        for (BooleanExpression predicate : predicates) {
            result = result.and(predicate);
        }
        return result;
    }
}

7. Testen Sie die Suchanfragen

Als Nächstes testen wir unsere Such-API.

Wir beginnen mit der Initialisierung der Datenbank mit einigen Benutzern, damit diese zum Testen bereitstehen:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { PersistenceConfig.class })
@Transactional
@Rollback
public class JPAQuerydslIntegrationTest {

    @Autowired
    private MyUserRepository repo;

    private MyUser userJohn;
    private MyUser userTom;

    @Before
    public void init() {
        userJohn = new MyUser();
        userJohn.setFirstName("John");
        userJohn.setLastName("Doe");
        userJohn.setEmail("[email protected]");
        userJohn.setAge(22);
        repo.save(userJohn);

        userTom = new MyUser();
        userTom.setFirstName("Tom");
        userTom.setLastName("Doe");
        userTom.setEmail("[email protected]");
        userTom.setAge(26);
        repo.save(userTom);
    }
}

Als nächstes sehen wir uns an, wie Sie Benutzer mitgiven last name finden:

@Test
public void givenLast_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("lastName", ":", "Doe");

    Iterable results = repo.findAll(builder.build());
    assertThat(results, containsInAnyOrder(userJohn, userTom));
}

Lassen Sie uns nun sehen, wie Sie einen Benutzer mit bestimmtenboth first and last name finden:

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("firstName", ":", "John").with("lastName", ":", "Doe");

    Iterable results = repo.findAll(builder.build());

    assertThat(results, contains(userJohn));
    assertThat(results, not(contains(userTom)));
}

Als nächstes sehen wir uns an, wie Sie einen Benutzer mit bestimmtenboth last name and minimum age finden

@Test
public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("lastName", ":", "Doe").with("age", ">", "25");

    Iterable results = repo.findAll(builder.build());

    assertThat(results, contains(userTom));
    assertThat(results, not(contains(userJohn)));
}

Lassen Sie uns nun sehen, wie Sie nachMyUser suchen, diedoesn’t actually exist:

@Test
public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("firstName", ":", "Adam").with("lastName", ":", "Fox");

    Iterable results = repo.findAll(builder.build());
    assertThat(results, emptyIterable());
}

Lassen Sie uns abschließend sehen, wie SieMyUsergiven only part of the first name finden - wie im folgenden Beispiel:

@Test
public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("firstName", ":", "jo");

    Iterable results = repo.findAll(builder.build());

    assertThat(results, contains(userJohn));
    assertThat(results, not(contains(userTom)));
}

8. UserController

Lassen Sie uns zum Schluss alles zusammenfügen und die REST-API erstellen.

Wir definieren einUserController, das eine einfache MethodefindAll() mit einem "search" -Parameter definiert, der in der Abfragezeichenfolge übergeben werden soll:

@Controller
public class UserController {

    @Autowired
    private MyUserRepository myUserRepository;

    @RequestMapping(method = RequestMethod.GET, value = "/myusers")
    @ResponseBody
    public Iterable search(@RequestParam(value = "search") String search) {
        MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder();

        if (search != null) {
            Pattern pattern = Pattern.compile("(\w+?)(:|<|>)(\w+?),");
            Matcher matcher = pattern.matcher(search + ",");
            while (matcher.find()) {
                builder.with(matcher.group(1), matcher.group(2), matcher.group(3));
            }
        }
        BooleanExpression exp = builder.build();
        return myUserRepository.findAll(exp);
    }
}

Hier ist ein Beispiel für eine Kurztest-URL:

http://localhost:8080/myusers?search=lastName:doe,age>25

Und die Antwort:

[{
    "id":2,
    "firstName":"tom",
    "lastName":"doe",
    "email":"[email protected]",
    "age":26
}]

9. Fazit

Dieser dritte Artikel befasste sich mitthe first steps of building a query language for a REST API und nutzte die Querydsl-Bibliothek.

Die Implementierung ist natürlich früh, kann aber leicht weiterentwickelt werden, um zusätzliche Operationen zu unterstützen.

Diefull implementation dieses Artikels befinden sich inthe GitHub project - dies ist ein Maven-basiertes Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.