DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • How Spring and Hibernate Simplify Web and Database Management
  • Functional Endpoints: Alternative to Controllers in WebFlux
  • Graceful Shutdown: Spring Framework vs Golang Web Services
  • Actuator Enhancements: Spring Framework 6.2 and Spring Boot 3.4

Trending

  • Cookies Revisited: A Networking Solution for Third-Party Cookies
  • Rust and WebAssembly: Unlocking High-Performance Web Apps
  • *You* Can Shape Trend Reports: Join DZone's Software Supply Chain Security Research
  • Segmentation Violation and How Rust Helps Overcome It
  1. DZone
  2. Coding
  3. Frameworks
  4. Specifications to the Rescue

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.

By 
John Vester user avatar
John Vester
DZone Core CORE ·
Mar. 22, 19 · Tutorial
Likes (9)
Comment
Save
Tweet
Share
111.9K Views

Join the DZone community and get the full member experience.

Join For Free

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!

Spring Framework

Opinions expressed by DZone contributors are their own.

Related

  • How Spring and Hibernate Simplify Web and Database Management
  • Functional Endpoints: Alternative to Controllers in WebFlux
  • Graceful Shutdown: Spring Framework vs Golang Web Services
  • Actuator Enhancements: Spring Framework 6.2 and Spring Boot 3.4

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!