{{announcement.body}}
{{announcement.title}}

Specifications to the Rescue

DZone 's Guide to

Specifications to the Rescue

Learn how Spring Boot, the JPA repository, and specification adoption can make it easy to provide filter solutions for your API.

· Java Zone ·
Free Resource

While working in a project recently, I utilized Spring Data JPA Specifications (org.springframework.data.jpa.domain.Specification) to provide some easy filtering for our RESTful API.  I thought I would talk about my approach and findings here, using a sample project I created in my GitLab repository.

The Real Use Case

For the actual project, there was a panel in our Angular application that displayed a set of images that can be dragged and dropped onto the canvas where images are being organized to formulate a page. The challenge is that each instance of the application will contain a large inventory of images for use. As a result, a filtering option was required.

The legacy application did not allow multiple/additive filters based upon the technologies that were employed. However, since we were using Spring Boot with JPA repositories to access the data, I was able to leverage Specifications to allow for additive filters to be utilized.

My Example

For my example, I created two Java-based domain objects to represent members at a fitness club.  The Member.class file contains information about a given member, and the Class.class contains classes that are available for attendance as a part of the gym membership.  Note: I really hesitated with using Class as a class name.

Member.java

@Data
@Entity
public class Member {
    @Id
    @GeneratedValue
    private long id;
    private String firstName;
    private String lastName;
    private String zipCode;
    private String interests;
    private boolean active;

    @JoinTable(name = "Member_Class",
            joinColumns = @JoinColumn(
                    name = "member_id",
                    referencedColumnName = "id"
            ),
            inverseJoinColumns = @JoinColumn(
                    name = "class_id",
                    referencedColumnName = "id"
            ))
    @ManyToMany
    private Set<Class> classes;

    public Member() { }
}


Class.java

@Data
@Entity
public class Class {
    @Id
    @GeneratedValue
    private long id;
    private String name;

    public Class() { }
}


When making the GET RESTful API call to the /members URI using the following cURL:

curl -X GET \
  http://localhost:9000/members


The following payload is returned from the in-memory H2 database that I configured for my Spring Boot project:

[
    {
        "id": 1,
        "firstName": "Jon",
        "lastName": "Anderson",
        "zipCode": "90215",
        "interests": "I like to write music and play racket sports",
        "active": true,
        "classes": [
            {
                "id": 102,
                "name": "Tennis"
            },
            {
                "id": 105,
                "name": "Swimming"
            }
        ]
    },
    {
        "id": 6,
        "firstName": "Geddy",
        "lastName": "Lee",
        "zipCode": "90212",
        "interests": "I enjoy playing racquetball too",
        "active": true,
        "classes": [
            {
                "id": 102,
                "name": "Tennis"
            }
        ]
    },
    {
        "id": 7,
        "firstName": "Alex",
        "lastName": "Lifeson",
        "zipCode": "90211",
        "interests": "I like staying in shape, drinking games",
        "active": true,
        "classes": [
            {
                "id": 104,
                "name": "FitCore 2000"
            },
            {
                "id": 105,
                "name": "Swimming"
            }
        ]
    },
    {
        "id": 8,
        "firstName": "Neil",
        "lastName": "Peart",
        "zipCode": "10010",
        "interests": "I enjoy cycling, writing and playing drums",
        "active": false,
        "classes": [
            {
                "id": 101,
                "name": "Spin"
            },
            {
                "id": 102,
                "name": "Tennis"
            },
            {
                "id": 105,
                "name": "Swimming"
            }
        ]
    },
    {
        "id": 2,
        "firstName": "Trevor",
        "lastName": "Rabin",
        "zipCode": "90215",
        "interests": "I am a guitar-playing point guard, who likes tennis too",
        "active": true,
        "classes": [
            {
                "id": 103,
                "name": "Basketball"
            }
        ]
    },
    {
        "id": 4,
        "firstName": "Chris",
        "lastName": "Squire",
        "zipCode": "33756",
        "interests": "",
        "active": false,
        "classes": [
            {
                "id": 101,
                "name": "Spin"
            },
            {
                "id": 102,
                "name": "Tennis"
            },
            {
                "id": 105,
                "name": "Swimming"
            }
        ]
    },
    {
        "id": 3,
        "firstName": "Rick",
        "lastName": "Wakeman",
        "zipCode": "02215",
        "interests": "I enjoy a good practical joke",
        "active": true,
        "classes": [
            {
                "id": 103,
                "name": "Basketball"
            }
        ]
    },
    {
        "id": 5,
        "firstName": "Alan",
        "lastName": "White",
        "zipCode": "90210",
        "interests": "I have no interests",
        "active": false,
        "classes": [
            {
                "id": 104,
                "name": "FitCore 2000"
            }
        ]
    }
]


Base Configuration

In order to implement Specifications, I had to update the MemberRepository interface to extend the JpaSpecificationExecutor class.

MemberRepository.java

@Repository
public interface MemberRepository extends JpaRepository<Member, Long>, JpaSpecificationExecutor { }


With the MemberRepository class set, I then created a BaseSpecification (abstract) class to include a Specification called getFilter():

BaseSpecification.java

public abstract class BaseSpecification<T, U> {
    public abstract Specification<T> getFilter(U request);
}


My thought is that all Specifications that utilize the BaseSpecification will utilize a getFilter()method.

Member Specification Development

With the groundwork in place, I was ready to create the MemberSpecification class, which extends the BaseSpecification class noted above.  The first step was override the getFilter() method:

MemberSpecification.java

@Override
public Specification<Member> getFilter(FilterRequest request) {
    return (root, query, cb) -> {
        query.distinct(true);
        query.orderBy(cb.asc(root.get("lastName")));
        return where(isActive(request.getActive())
                .and(inZipCode(request.getZipFilter())))
                .toPredicate(root, query, cb);
    };
}


The getFilter() method will return distinct results, ordered by lastName.  The resulting data set will be the result of running theisActive() andinZipCode() methods, which are displayed below:

MemberSpecification.java

private Specification<Member> isActive(Boolean isActive) {
    return (root, query, cb) -> {
        if (isActive != null) {
            return cb.equal(root.get("active"), isActive);
        } else {
            return null;
        }
    };
}

private Specification<Member> inZipCode(String zipFilter) {
    return (root, query, cb) -> {
        if (zipFilter != null) {
            return cb.like(root.get("zipCode"), cb.literal(zipFilter + "%"));
        } else {
            return null;
        }
    };
}


With the above logic in place, the Members list can be easily filtered to find active/inactive members and those who have a zipCode, which begins with the provided zipFilter.  So, a zipFilter of "123" will return all zip codes that begin with "123" — keep in mind, this is just a simple example to show multiple filters.

Adding searchString Parameter

Like my actual application, the product owner wanted the end-user to have the ability to provide a search term, which could also be used to filter the data. In my example, I decided that the search term would be called searchString and would search not only the interests attribute on theMember.class but any name in the Class.class in use.

To find matching interests for the searchString, I created the following hasString() method:

MemberSpecification.java

public Specification<Member> hasString(String searchString) {
    return (root, query, cb) -> {
        if (searchString != null) {
            return cb.like(cb.lower(root.get("interests")), cb.lower(cb.literal("%" + searchString + "%")));
        } else {
            return null;
        }
    };
}


In order to locate matching classes, I first updated the ClassRepository to include a helper query:

ClassRepository.java

public interface ClassRepository extends JpaRepository<Class, Long>, JpaSpecificationExecutor {
    List<Class> findAllByNameContainsIgnoreCase(String searchString);
}


Next, I added the hasClasses() method to the MemberSpecification class:

MemberSpecification.java

public Specification<Member> hasClasses(String searchString) {
    return (root, query, cb) -> {
        if (searchString != null) {
            List<Class> classes = classRepository.findAllByNameContainsIgnoreCase(searchString);

            if (!CollectionUtils.isEmpty(classes)) {
                SetJoin<Member, Class> masterClassJoin = root.joinSet("classes", JoinType.LEFT);
                List<Predicate> predicates = new ArrayList<>();
                predicates.add(masterClassJoin.in(new HashSet<>(classes)));
                Predicate[] p = predicates.toArray(new Predicate[predicates.size()]);
                return cb.or(p);
            }
        }

        return null;
    };
}


Applying Specifications

With everything now in place, I simply needed to reference the Specifications in my service class:

MemberService.java

public List<Member> getMembers(FilterRequest filter, String searchString) {
    return memberRepository.findAll(Specification.where(memberSpecification.hasString(searchString)
            .or(memberSpecification.hasClasses(searchString)))
            .and(memberSpecification.getFilter(filter)));
}


As a result, when all Member objects are being requested, if there is a searchString, it will be utilized to locate LIKE matches in the interests attribute or the name attribute on the Class objects. Additionally, filters by active/inactive status and/or zipCode will also be returned.

Specifications in Action

Using the same dataset, the following CURL:

curl -X GET \
  'http://localhost:9000/members?active=true&zipFilter=902&searchString=tennis'


Returns the filtered data:

[
    {
        "id": 1,
        "firstName": "Jon",
        "lastName": "Anderson",
        "zipCode": "90215",
        "interests": "I like to write music and play racket sports",
        "active": true,
        "classes": [
            {
                "id": 102,
                "name": "Tennis"
            },
            {
                "id": 105,
                "name": "Swimming"
            }
        ]
    },
    {
        "id": 6,
        "firstName": "Geddy",
        "lastName": "Lee",
        "zipCode": "90212",
        "interests": "I enjoy playing racquetball too",
        "active": true,
        "classes": [
            {
                "id": 102,
                "name": "Tennis"
            }
        ]
    },
    {
        "id": 2,
        "firstName": "Trevor",
        "lastName": "Rabin",
        "zipCode": "90215",
        "interests": "I am a guitar-playing point guard, who likes tennis too",
        "active": true,
        "classes": [
            {
                "id": 103,
                "name": "Basketball"
            }
        ]
    }
]


All three results are active members, with a zipCode that begins with 902 and the searchTerm tennis being matched in either the interests attribute or the classes.name list/set.

Source Code

To see the complete source code, simply launch the following URL:

https://gitlab.com/johnjvester/jpa-spec

Have a really great day!

Topics:
java ,spring ,boot ,jpa ,specifications ,api ,development ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}