Broadleaf Microservices
  • v1.0.0-latest-prod

Querying Data Through Repositories

Broadleaf leverages Spring Data as the vehicle for performing CRUD operations on a backing datastore. Several layers are employed to maintain abstraction between the service layer and one or more platform-specific repository layers (e.g. MongoDB or JPA). We’ll go over the general structure of a microservice and where the repository fits in the MicroService Anatomy section. Here, we’ll focus on the repository layer, how it is modeled for maximum flexibility, and how data queries are constructed.

Repository Design

Spring data repositories are generally tied to a database plaform (e.g. Spring-Data-MongoDB), or to a platform type (e.g. Spring-Data-JPA). The application is configured for the desired repository type based on the chosen target persistence platform. This document focuses on a special type of repository, the TrackableRepository, which provides special narrowing behavior related to sandboxing, and various other form of entity discrimination. See Sandboxing In Detail for more specific information on the entity structure and types of discrimination supported. However, note that any vanilla Spring Data repository may be employed (or any Java persistence mechanism for that matter) if the additional narrowing features of TrackableRepository are not needed, or are prohibitive in some way.

Broadleaf will generally start with a interface definition that extends from TrackableRepository. The generics are kept the same as TrackableRepository (i.e. a specific domain type is not declared yet). The intent for this interface is to hold query method definitions, but to not be used as the final interface on which to build an actual Repository instance. For this reason, the interface is annotated with the @NoRepositoryBean annotation. Extending from TrackableRepository provides a number of out-of-the-box CRUD operations so that query methods in this extending interface will generally focus on fetch related behavior. See here for more information on Query Methods. Query methods are interesting in that their name drives automatic query construction creation by Spring Data. The idea at this level is that query methods are simple enough to work for multiple backing data platforms. Here’s an example for a general Product repository interface.

@NoRepositoryBean
public interface MyRepository<D extends Trackable, I> extends TrackableRepository<D, I> {

    Page<D> findAllByNameContainingIgnoreCase(String query, Pageable page, ContextInfo contextInfo);

}

Next, one or more platform specific repository interfaces are created that extend from the previous interface. These interfaces do indeed call out the generics specific to the platform domain class and declare an explicit @Repository annotation.

@Repository
@Narrow(JpaNarrowExecutor.class)
public interface JpaMyRepository<D extends JpaMyItem> extends MyRepository<D, String> {

}
Note
A NarrowExecutor class may be declared in a @Narrow annotation on the final repository interface. The NarrowExecutor implementation is database platform specific and is responsible for providing additional narrowing and/or filtering behavior beyond what is already provided in the Query. This is generally used to narrow results to a specific sandbox, or filter results based on visible catalog(s).

This second interface will result in a real proxy object created and injected by Spring into other components that wish to exercise persistence behavior, but those components will view the object as an instance of the earlier interface, thereby allowing those components to be largely agnostic to the backing database platform. Because of this, the latter interface is generally empty. However, it may contain additional query methods if there is a query approach being used that is truly only possible on the targeted database platform and it is desirable to expose an API to exercise that approach. This is different than providing a common API, but implementing a query to support that API differently per platform, which we’ll discuss next.

Registering the Repository

The common methods in the TrackableRepository interface have platform-specific implementations (e.g. JpaTrackableRepository). In order for Spring Data to initialize your repository interface with this base functionality, you must specify a repositoryFactoryBeanClass:

@EnableJpaRepositories(repositoryFactoryBeanClass = NarrowingJpaRepositoryFactoryBean.class,
    basePackageClasses = JpaMyRepository.class)
@Configuration
public class JpaRepositoryConfig {

}

The base classes that the @EnableJpaRepositories targets can only contain Trackable repositories.

Note
If using the FlexPackage approach for collapsing multiple services into a single deployment/execution unit, then more configuration is required to setup the repository for data routing. See Deployment Flexibility and Data Routing for more information.

More Control Than Query Methods Alone

It is common to have a repository API where one or more of the individual method implementations must be achieved in a way more custom than what is allowed by simple repository dynamic query methods. In such cases, fragment interfaces are employed in a way consistent with standard Spring Data practices: Customizing Repositories. A new interface is added to the mix that declares the additional API.

public interface AdditionalRepository<D> {

    List<D> findAllByContextIdIn(
            Iterable<String> contextIds,
            @Nullable ContextInfo contextInfo);

}

This fragment should be added to the generic interface describing your overall repository contract.

@NoRepositoryBean
public interface MyRepository<D extends Trackable, I> extends TrackableRepository<D, I>, AdditionalRepository<D> {

    Page<D> findAllByNameContainingIgnoreCase(String query, Pageable page, ContextInfo contextInfo);

}

Secondly, a new platform-specific implementation is added to support this fragment. This implementation doesn’t rely on Spring Data and can be injected with any additional components necessary to perform the persistence task(s).

@Component("myRepositoryImpl")
public class JpaCustomizedMyRepository<D extends JpaMyItem> implements AdditionalRepository<D> {

    public List<D> findAllByContextIdIn(
            Iterable<String> contextIds,
            @Nullable ContextInfo contextInfo);
        ...
    }
}
Note
The bean name specified in the @Component declaration is what ties the implementation to the original repository. The name matching the original repository interface and the Impl postfix are important.

In the implementation class, you will need to implement the methods demanded by whatever fragment interfaces are implemented, but you may also implement methods from the original repository, in which case your implementation will win over the default Spring Data implementation. When replacing method implementations from the original repository, you don’t need to implement the original repository interface - rather - just implement the method and Spring Data will detect that your customization implementation has the same method signature as a method from the original repository interface and will choose your implementation instead.

Note
When overriding a method from the original repository interface, it is important to match the same method signature as the interface. Therefore, if you want to override the D save(D entity) method from TrackableRepository, your custom method implementation will need to have the signature of Trackable save (Trackable entity), since TrackableRepository has generic declaration such that D extends Trackable.

Custom Criteria in a Repository Fragment

When creating fragment implementations, it is necessary to explicitly build JPA CriteriaQuery instances to run against the platform.

@Component("AdditionalRepositoryImpl")
public class JpaAdditionalRepository<D extends JpaProduct> implements AdditionalRepository<D> {

    @Override
    public List<D> findAllByContextIdIn(
            Iterable<String> contextIds,
            @Nullable ContextInfo contextInfo) {
        ...
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<D> criteria = builder.createQuery(entityType);
        Root<D> topRoot = criteria.from(entityType);
        criteria.select(topRoot);

        List<Predicate> predicates = new ArrayList<>();
        Map<String, Object> params = new HashMap<>();
        predicates.add(topRoot.get(CONTEXT_ID).in(builder.parameter(List.class, CONTEXT_IDS)));
        params.put(CONTEXT_IDS, contextIdList);

        criteria.where(predicates.toArray(new Predicate[0]));

        JpaCriterias<D> jpaCriterias = new JpaCriterias<>(criteria, null, params);
        return narrowingHelper.fetchAll(jpaCriterias, entityType, null, contextInfo);
    }
}

JpaCriterias is a special Broadleaf construct designed to hold JPA Criteria information, as well as bindable parameters (the system will bind the parameters for you as part of the narrowing lifecycle).

Note
By default, Broadleaf ships with DefaultJpaNarrowingHelper, which can be instantiated directly as part of a fragment bean constructor.
Note
You can also extend and customize existing Broadleaf repositories using techniques similar to what’s described above. See extensibility for more information.
Caution
It is a known limitation that to use the narrowing features of TrackableRepository, you must either use an undecorated dynamic query method, or create a JPA criteria query as demonstrated above. If you add a Spring Data @Query annotation to a repository method, the resulting query will not benefit from trackable narrowing. Furthermore, in custom repository fragments, you must use the JpaNarrowingHelper to pass your query for narrowed execution, and this flow depends on the use of JPA Criteria constructs. If you use other JPA approaches (including named queries, and the like), your queries will execute, but they will not benefit from the automatic addition of narrowing criteria. This may or may not be suitable for your use case, so be aware of the limitation.