REST-Abfragesprache mit Spring- und JPA-Kriterien

REST-Abfragesprache mit Spring- und JPA-Kriterien

1. Überblick

In diesem ersten Artikel vonthis new series werden wira simple query language for a REST API untersuchen. Wir werden Spring für die REST-API und JPA 2-Kriterien für die Persistenzaspekte gut nutzen.

Why a query language? Weil - für eine ausreichend komplexe API - das Suchen / Filtern Ihrer Ressourcen nach sehr einfachen Feldern einfach nicht ausreicht. Eine Abfragesprache ist flexibler und ermöglicht es Ihnen, genau die Ressourcen zu filtern, die Sie benötigen.

2. User Entität

Lassen Sie uns zunächst die einfache Entität vorschlagen, die wir für unsere Filter- / Such-API verwenden werden - ein grundlegendesUser:

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

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

    private int age;
}

3. Filtern mitCriteriaBuilder

Kommen wir nun zum Kern des Problems - der Abfrage in der Persistenzschicht.

Das Erstellen einer Abfrageabstraktion ist eine Frage des Gleichgewichts. Wir brauchen einerseits ein hohes Maß an Flexibilität und andererseits müssen wir die Komplexität beherrschbar halten. Auf hohem Niveau ist die Funktionalität einfach -you pass in some constraints and you get back some results.

Mal sehen, wie das funktioniert:

@Repository
public class UserDAO implements IUserDAO {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List searchUser(List params) {
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery query = builder.createQuery(User.class);
        Root r = query.from(User.class);

        Predicate predicate = builder.conjunction();

        UserSearchQueryCriteriaConsumer searchConsumer =
          new UserSearchQueryCriteriaConsumer(predicate, builder, r);
        params.stream().forEach(searchConsumer);
        predicate = searchConsumer.getPredicate();
        query.where(predicate);

        List result = entityManager.createQuery(query).getResultList();
        return result;
    }

    @Override
    public void save(User entity) {
        entityManager.persist(entity);
    }
}

Werfen wir einen Blick auf die KlasseUserSearchQueryCriteriaConsumer:

public class UserSearchQueryCriteriaConsumer implements Consumer{

    private Predicate predicate;
    private CriteriaBuilder builder;
    private Root r;

    @Override
    public void accept(SearchCriteria param) {
        if (param.getOperation().equalsIgnoreCase(">")) {
            predicate = builder.and(predicate, builder
              .greaterThanOrEqualTo(r.get(param.getKey()), param.getValue().toString()));
        } else if (param.getOperation().equalsIgnoreCase("<")) {
            predicate = builder.and(predicate, builder.lessThanOrEqualTo(
              r.get(param.getKey()), param.getValue().toString()));
        } else if (param.getOperation().equalsIgnoreCase(":")) {
            if (r.get(param.getKey()).getJavaType() == String.class) {
                predicate = builder.and(predicate, builder.like(
                  r.get(param.getKey()), "%" + param.getValue() + "%"));
            } else {
                predicate = builder.and(predicate, builder.equal(
                  r.get(param.getKey()), param.getValue()));
            }
        }
    }

    // standard constructor, getter, setter
}

Wie Sie sehen können, verwendet diesearchUser-API eine Liste sehr einfacher Einschränkungen, erstellt eine Abfrage basierend auf diesen Einschränkungen, führt die Suche durch und gibt die Ergebnisse zurück.

Die Constraint-Klasse ist ebenfalls recht einfach:

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

Die Implementierung vonSearchCriteriaenthält unsereQuery-Parameter:

  • key: Wird verwendet, um den Feldnamen zu speichern - zum Beispiel:firstName,age, ... usw.

  • operation: Wird verwendet, um die Operation zu halten - zum Beispiel: Gleichheit, kleiner als, ... usw.

  • value: Wird verwendet, um den Feldwert zu halten - zum Beispiel: John, 25,… usw.

4. Testen Sie die Suchanfragen

Testen Sie jetzt unseren Suchmechanismus, um sicherzustellen, dass er Wasser enthält.

Zunächst initialisieren wir unsere Datenbank zum Testen, indem wir zwei Benutzer hinzufügen - wie im folgenden Beispiel:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { PersistenceConfig.class })
@Transactional
@TransactionConfiguration
public class JPACriteriaQueryTest {

    @Autowired
    private IUserDAO userApi;

    private User userJohn;

    private User userTom;

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

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

Lassen Sie uns nun einUser mit bestimmtenfirstName undlastName erhalten - wie im folgenden Beispiel:

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    List params = new ArrayList();
    params.add(new SearchCriteria("firstName", ":", "John"));
    params.add(new SearchCriteria("lastName", ":", "Doe"));

    List results = userApi.searchUser(params);

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

Als nächstes erhalten wirList vonUser mit denselbenlastName:

@Test
public void givenLast_whenGettingListOfUsers_thenCorrect() {
    List params = new ArrayList();
    params.add(new SearchCriteria("lastName", ":", "Doe"));

    List results = userApi.searchUser(params);
    assertThat(userJohn, isIn(results));
    assertThat(userTom, isIn(results));
}

Als nächstes erhalten wir Benutzer mitagegreater than or equal 25:

@Test
public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() {
    List params = new ArrayList();
    params.add(new SearchCriteria("lastName", ":", "Doe"));
    params.add(new SearchCriteria("age", ">", "25"));

    List results = userApi.searchUser(params);

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

Als nächstes suchen wir nach Benutzern, diedon’t actually exist:

@Test
public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() {
    List params = new ArrayList();
    params.add(new SearchCriteria("firstName", ":", "Adam"));
    params.add(new SearchCriteria("lastName", ":", "Fox"));

    List results = userApi.searchUser(params);
    assertThat(userJohn, not(isIn(results)));
    assertThat(userTom, not(isIn(results)));
}

Lassen Sie uns abschließend nach Benutzern suchen, denen nurpartialfirstName gegeben sind:

@Test
public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() {
    List params = new ArrayList();
    params.add(new SearchCriteria("firstName", ":", "jo"));

    List results = userApi.searchUser(params);

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

6. DieUserController

Lassen Sie uns nun die Persistenzunterstützung für diese flexible Suche mit unserer REST-API verbinden.

Wir werden ein einfachesUserController einrichten - mit einemfindAll()using the “search” to pass in the entire search/filter expression:

@Controller
public class UserController {

    @Autowired
    private IUserDao api;

    @RequestMapping(method = RequestMethod.GET, value = "/users")
    @ResponseBody
    public List findAll(@RequestParam(value = "search", required = false) String search) {
        List params = new ArrayList();
        if (search != null) {
            Pattern pattern = Pattern.compile("(\w+?)(:|<|>)(\w+?),");
            Matcher matcher = pattern.matcher(search + ",");
            while (matcher.find()) {
                params.add(new SearchCriteria(matcher.group(1),
                  matcher.group(2), matcher.group(3)));
            }
        }
        return api.searchUser(params);
    }
}

Beachten Sie, wie wir einfach unsere Suchkriterienobjekte aus dem Suchausdruck erstellen.

Wir sind jetzt an dem Punkt angelangt, an dem wir mit der API spielen und sicherstellen können, dass alles richtig funktioniert:

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

Und hier ist seine Antwort:

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

7. Fazit

Diese einfache, aber leistungsstarke Implementierung ermöglicht eine Reihe von intelligenten Filtern in einer REST-API. Ja, es ist immer noch rau an den Rändern und kann verbessert werden (und wird im nächsten Artikel verbessert) - aber es ist ein solider Ausgangspunkt, um diese Art von Filterfunktionalität in Ihren APIs zu implementieren.

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.