Skip to content

Latest commit

 

History

History
654 lines (535 loc) · 18.8 KB

README.adoc

File metadata and controls

654 lines (535 loc) · 18.8 KB

Quarkus Criteria Extension

Usage

  • Add the following dependency:

    <dependency>
        <groupId>com.github.quarkus-criteria</groupId>
        <artifactId>quarkus-criteria</artifactId>
        <version>1.0.0</version>
    </dependency>
  • Create a JPA entity which implements PersistenceEntity or extend BaseEntity

    @Entity
    @Table(name = "car")
    public class Car extends BaseEntity {
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Integer id;
    
        @Column(name = "model")
        private String model;
    
        @Column(name = "name")
        private String name;
    
        @Column(name = "price")
        private Double price;
    
        @OneToOne
        private Brand brand;
    
        @oneToMany
        private List<CarSalesPoint> salesPoints;
    
        @Version
        private Integer version;
    
        @Override
        public Integer getId() {
            return id;
        }
  • To have type safe queries you need to enable JPA metamodel generation on your build:

    <plugin>
        <groupId>org.bsc.maven</groupId>
        <artifactId>maven-processor-plugin</artifactId>
        <version>3.3.2</version>
        <executions>
            <execution>
                <id>process</id>
                <phase>generate-sources</phase>
                <goals>
                    <goal>process</goal>
                </goals>
                <configuration>
                    <processors>
                        <processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>
                    </processors>
                </configuration>
            </execution>
        </executions>
        <dependencies>
            <dependency>
                <groupId>org.hibernate</groupId>
                <artifactId>hibernate-jpamodelgen</artifactId>
                <version>5.2.12.Final</version>
            </dependency>
        </dependencies>
    </plugin>
    💡
    See this tutorial for configuring it on your IDE.
  • Now you’re ready to use DeltaSpike type safe fluent criteria API on your Quarkus application

For examples below consider the following entity associations:

entities

Examples

The easiest way to use quarkus criteria is via generic injection of CrudService:

@Inject
@Service
CrudService<Car> crudService;

Below are some query examples:

List cars by brand name

List<Car> carsFound = crudService.criteria()
        .fetch(Car_.brand)
        .join(Car_.brand, crudService.where(Brand.class)
            .eqIgnoreCase(Brand_.name, "tesla"))
        .getResultList();

assertThat(carsFound).isNotNull().hasSize(2)
        .extracting("name")
        .contains("Model S", "Model X");

List brands by sales point address

@Inject
CrudService<Brand> brandCrud;

List<Brand> brands = brandCrud.criteria().distinct()
        .join(Brand_.cars, brandCrud.where(Car.class)
                .join(Car_.carSalesPoints, brandCrud.where(CarSalesPoint.class)
                     .join(CarSalesPoint_.salesPoint, brandCrud.where(SalesPoint.class)
                        .eq(SalesPoint_.address, "Tesla HQ address")))
              )
        .getResultList();
assertThat(brands).isNotNull().hasSize(1)
        .extracting(brand -> brand.getName())
        .contains("Tesla");

List cars by model, brand name and sales point address

public List<Car> listCarsByModelBrandAndSalesPointAddress() {
    return criteria()
            .distinct()
            .fetch(Car_.brand)
            .join(Car_.brand, where(Brand.class)
                    .or(criteria(Brand.class).eq(Brand_.name, "Nissan"),
                            criteria(Brand.class).eq(Brand_.name, "Ford")))
            .join(Car_.carSalesPoints, where(CarSalesPoint.class)
                    .join(CarSalesPoint_.salesPoint, where(SalesPoint.class)
                            .eqIgnoreCase(SalesPoint_.address, "ford motors address")))
            .or(criteria().likeIgnoreCase(Car_.model, "%tanium"),
                    criteria().eq(Car_.name, "Sentra"))
            .getResultList();
    }

Above criteria will generate following select:

Hibernate:
    select
        distinct car0_.id as id1_1_0_,
        brand4_.id as id1_0_1_,
        car0_.brand_id as brand_id6_1_0_,
        car0_.model as model2_1_0_,
        car0_.name as name3_1_0_,
        car0_.price as price4_1_0_,
        car0_.version as version5_1_0_,
        brand4_.name as name2_0_1_
    from
        car car0_
    inner join
        brand brand1_
            on car0_.brand_id=brand1_.id
    inner join
        car_sales_point carsalespo2_
            on car0_.id=carsalespo2_.car_id
    inner join
        sales_point salespoint3_
            on carsalespo2_.SALESPOINTS_ID1=salespoint3_.ID1
            and carsalespo2_.SALESPOINTS_ID2=salespoint3_.ID2
    inner join
        brand brand4_
            on car0_.brand_id=brand4_.id
    where
        (
            brand1_.name=?
            or brand1_.name=?
        )
        and upper(salespoint3_.address)=?
        and (
            upper(car0_.model) like ?
            or car0_.name=?
        )
💡

You can also inherit from CrudService, this way you can invoke where, criteria() and other methods directly from superclass instead of calling them via crudService instance:

@Transactional(Transactional.TxType.SUPPORTS)
public class CarService extends CrudService<Car> implements Serializable {

 public List<CarWithNameAndPrice> getCarsAndMapToDTO() {
        List<CarWithNameAndPrice> carsDTO = criteria()
                .select(CarWithNameAndPrice.class, attribute(Car_.name), attribute(Car_.price)) (1)
                .join(Car_.brand, where(Brand.class)
                        .or(criteria(Brand.class)
                                        .eq(Brand_.name, "Nissan"),
                                criteria(Brand.class).eq(Brand_.name, "Tesla")))
                .join(Car_.salesPoints, where(SalesPoint.class)
                        .likeIgnoreCase(SalesPoint_.name, "%Tesla%")).getResultList();
        return carsDTO;

}
  1. Select fields and target DTO to map results

Aggregation function

Double result = criteria()
    .select(Double.class, sum(Car_.price))
    .likeIgnoreCase(Car_.model, model)
    .getSingleResult();

Count by criteria

long count = crudService.count(crudService.criteria()
            .likeIgnoreCase(Car_.model, "%porche%")
            .gtOrEq(Car_.price, 10000D));

Insert

List.of(new Brand().setName("Nissan"),
        new Brand().setName("Ford"),
        new Brand().setName("Tesla"))
          .forEach(brandCrud::insert);

Delete in batches

int deleted = crudService.removeBatch(crudService.criteria().getResultList(), 5);
assertThat(deleted).isEqualTo(10);
assertThat(crudService.count()).isEqualTo(0L);

Pagination

    /**
     * curl -X GET http://localhost:8080/api/cars -v
     */
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response list(@QueryParam("first") @DefaultValue("0") Integer startPosition,
                         @QueryParam("pageSize") @DefaultValue("10") Integer maxResult,
                         @QueryParam("sortField") @DefaultValue("id") String sortField,
                         @QueryParam("sortField") @DefaultValue("ASCENDING") SortType sortType,
                         @QueryParam("name") @DefaultValue("") String name,
                         @QueryParam("model") @DefaultValue("") String model,
                         @QueryParam("price") Double price,
                         @QueryParam("brandId") Long brandId
                         ) {

        Filter<Car> carFilter = new Filter<>(new Car().setName(name)
                .setModel(model)
                .setPrice(price))
                .setFirst(startPosition)
                .setPageSize(maxResult)
                .setSortType(sortType)
                .setSortField(sortField);
        if(brandId != null) {
            carFilter.getEntity().setBrand(new Brand(brandId));
        }

        return ok(carService.paginate(carFilter)).build();
    }
💡
paginate method will use filter to get pagination information and also call configPagination on your service so you can add restrictions or fetch additional fields, see CarService for example.
💡

For more examples see:

BaseCriteriaSupport

If you don’t want CRUD support you can inherit directly from BaseCriteriaSupport:

@ApplicationScoped
public class CarCriteria extends BaseCriteriaSupport<Car> {
    /**
     * getEntityManager().createQuery("SELECT SUM(c.price) FROM Car c WHERE upper(c.model) like :model", Double.class)
     *                 .setParameter("model", model).getSingleResult();
     */
    public Double getTotalPriceByModel(String model) {
        return criteria()
                .select(Double.class, sum(Car_.price))
                .likeIgnoreCase(Car_.model, model)
                .getSingleResult();
    }
}

Criteria by example

You can query by example using exampleBuilder:

 Car carExample = new Car().model("Ferrari");
 List<Car> cars = carService
      .exampleBuilder.of(carExample)
      .with(Car_.model) (1)
      .build()
      .getResultList();
  1. select which attributes to consider from example entity, if no attribute is provided then non null properties from example entity will be considered.

Comparison operation

You can also choice the comparison operation to be used when comparing example attribute value:

Car carExample = new Car().setModel("%rrari");
List<Car> cars = carService
        .exampleBuilder.of(carExample)
        .with(ComparisonOperation.LIKE_IGNORE_CASE, Car_.model)
        .build()
        .getResultList();

If no comparisonOperation is provided then EQ will be used.

Multiple attributes

You can also provide multiple attributes:

Car carExample = new Car().setModel("porche").setName("%avenger");
List<Car> cars = crudService
        .exampleBuilder
        .of(carExample)
        .with(ComparisonOperation.LIKE_IGNORE_CASE, Car_.name, Car_.model)
        .build()
        .getResultList();

Which will generate following query:

Hibernate:
    select
        car0_.id as id1_1_,
        car0_.brand_id as brand_id6_1_,
        car0_.model as model2_1_,
        car0_.name as name3_1_,
        car0_.price as price4_1_,
        car0_.version as version5_1_
    from
        car car0_
    where
        (
            upper(car0_.name) like ?
        )
        and (
            upper(car0_.model) like ?
        )

Associations

Associations will also be queried:

SalesPoint salesPoint = new SalesPoint().setAddress("Tesla HQ address");
CarSalesPoint carSalesPointExample = new CarSalesPoint().setSalesPoint(salesPoint);

List<CarSalesPoint> carSalesPointsFound = carSalesPointCrud
        .exampleBuilder.of(carSalesPointExample)
        .with(SalesPoint_.address)
        .build()
        .distinct()
        .fetch(CarSalesPoint_.salesPoint) (1)
        .getResultList();
assertThat(carSalesPointsFound).isNotNull().hasSize(2);
List<Car> carsFound = carSalesPointsFound.stream()
        .map(carSalesPoint -> carSalesPoint.getCar())
        .collect(Collectors.toUnmodifiableList());
assertThat(carsFound).isNotNull().hasSize(2)
        .extracting("name")
        .contains("Model S", "Model X");
  1. Fetch association attribute in result list

List brands by sales point address

SalesPoint salesPoint = new SalesPoint()
        .setAddress("Tesla HQ address");
Set<Car> cars = Set.of(new Car().addSalesPoint(salesPoint));
Brand brandExample = new Brand().setCars(cars);

List<Brand> brands = brandCrud.exampleBuilder
        .of(brandExample)
        .with(EQ, SalesPoint_.address)
        .build()
        .distinct()
        .getResultList();
assertThat(brands).isNotNull().hasSize(1)
        .extracting(brand -> brand.getName())
        .contains("Tesla");

The example criteria above will generate following select:

Hibernate:
    select
        distinct brand0_.id as id1_0_,
        brand0_.name as name2_0_
    from
        brand brand0_
    inner join
        car cars1_
            on brand0_.id=cars1_.brand_id
    inner join
        car_sales_point carsalespo2_
            on cars1_.id=carsalespo2_.car_id
    inner join
        sales_point salespoint3_
            on carsalespo2_.SALESPOINTS_ID1=salespoint3_.ID1
            and carsalespo2_.SALESPOINTS_ID2=salespoint3_.ID2
    where
        salespoint3_.address=?

Different operation for each attribute

Brand brand = new Brand().setName("%ssan");
Car carExample = new Car().setModel("SE").setName("%tra")
        .setPrice(12.999)
        .setBrand(brand);
Car car = (Car) carService
        .exampleBuilder.of(carExample)
        .with(Car_.model) //EQ
        .with(LIKE_IGNORE_CASE, Brand_.name, Car_.name)
        .with(LT_OR_EQ, Car_.price)
        .build()
        .fetch(Car_.brand)
        .getSingleResult();
assertThat(car).isNotNull()
        .extracting(Car::getName, Car::getModel, Car::getBrand)
        .contains("Sentra", "SE", new Brand(2L));//brand id=2 is nissan

The following select is generated from above example:

Hibernate:
    select
        car0_.id as id1_1_0_,
        brand2_.id as id1_0_1_,
        car0_.brand_id as brand_id6_1_0_,
        car0_.model as model2_1_0_,
        car0_.name as name3_1_0_,
        car0_.price as price4_1_0_,
        car0_.version as version5_1_0_,
        brand2_.name as name2_0_1_
    from
        car car0_
    inner join
        brand brand1_
            on car0_.brand_id=brand1_.id
    inner join
        brand brand2_
            on car0_.brand_id=brand2_.id
    where
        car0_.model=?
        and (
            upper(brand1_.name) like ?
        )
        and (
            upper(car0_.name) like ?
        )
        and car0_.price<=12.999

Using an or expression

Car carExample = new Car()
        .setName("Fusion")
        .setModel("S")
        .setPrice(10.000D)
        .setBrand(new Brand().setName("Nissan"));

List<Car> cars = crudService
        .exampleBuilder.of(carExample)
        .with(GT_OR_EQ, Car_.price)
        .or(Car_.name, Car_.model, Brand_.name)
        .build()
        .getResultList();
assertThat(cars).isNotNull()
        .extracting(Car::getName)
        .contains("Fusion", "Sentra", "Model S")
        .doesNotContain("Model X");

Following select will be generated:

Hibernate:
    select
        car0_.id as id1_1_,
        car0_.brand_id as brand_id6_1_,
        car0_.model as model2_1_,
        car0_.name as name3_1_,
        car0_.price as price4_1_,
        car0_.version as version5_1_
    from
        car car0_
    inner join
        brand brand1_
            on car0_.brand_id=brand1_.id
    where
        car0_.price>=10.0
        or car0_.name=?
        or car0_.model=?
        or brand1_.name=?
💡
More examples can be found here.

Integration tests

Integration tests are located in it-tests module, to run them just run maven command:

mvn test

It tests cmdline app

It tests module has a sample cmdline app, to run it use:

mvn package && java -jar target/quarkus-criteria-it-runner.jar

Or run via quarkus:dev:

mvn compile quarkus:dev

System tests

System tests are located in system-tests module, to run the tests use following maven command:

mvn test

Native mode

To run system tests in native mode:

mvn verify -Pnative

To run native tests inside docker (without the need to install GraalVM):

  • MacOS:

    mvn verify -Dquarkus.native.container-build=true -Dquarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-mandrel:20.3-java11 -Pnative

  • Linux

    mvn verify -Dquarkus.native.container-build=true -Dquarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-native-image:20.3.0-java11 -Pnative

System tests rest application

The system tests example application needs a PostgreSQL database running, use the following docker command to spin up one:

docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 --name quarkus_test -e POSTGRES_USER=car -e POSTGRES_PASSWORD=car -e POSTGRES_DB=cars -p 5432:5432 postgres:11.5

After that just build and start the application:

cd system-tests && mvn package -DskipTests && java -jar target/quarkus-criteria-st-runner.jar

Snapshots

Snapshots are available in maven central, to use it just add the following snippet in your pom.xml:

<repositories>
    <repository>
        <snapshots/>
        <id>snapshots</id>
        <name>libs-snapshot</name>
        <url>https://oss.sonatype.org/content/repositories/snapshots</url>
    </repository>
</repositories>