diff --git a/persistence-modules/spring-data-jpa-filtering/README.md b/persistence-modules/spring-data-jpa-filtering/README.md
new file mode 100644
index 0000000000..fe2f181154
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/README.md
@@ -0,0 +1,19 @@
+## Spring Data JPA - Filtering
+
+This module contains articles about filtering data using Spring Data JPA
+
+### Relevant Articles:
+- [An Advanced Tagging Implementation with JPA](https://www.baeldung.com/jpa-tagging-advanced)
+- [A Simple Tagging Implementation with JPA](https://www.baeldung.com/jpa-tagging)
+- [Spring Data JPA and Null Parameters](https://www.baeldung.com/spring-data-jpa-null-parameters)
+- [Spring Data JPA Projections](https://www.baeldung.com/spring-data-jpa-projections)
+
+### Eclipse Config
+After importing the project into Eclipse, you may see the following error:
+"No persistence xml file found in project"
+
+This can be ignored:
+- Project -> Properties -> Java Persistance -> JPA -> Error/Warnings -> Select Ignore on "No persistence xml file found in project"
+Or:
+- Eclipse -> Preferences - Validation - disable the "Build" execution of the JPA Validator
+
diff --git a/persistence-modules/spring-data-jpa-filtering/pom.xml b/persistence-modules/spring-data-jpa-filtering/pom.xml
new file mode 100644
index 0000000000..2039ef1a37
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/pom.xml
@@ -0,0 +1,77 @@
+
+
+ 4.0.0
+ spring-data-jpa-filtering
+ spring-data-jpa-filtering
+
+
+ com.baeldung
+ parent-boot-2
+ 0.0.1-SNAPSHOT
+ ../../parent-boot-2
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.hibernate
+ hibernate-ehcache
+
+
+ org.hibernate
+ hibernate-envers
+
+
+
+ com.h2database
+ h2
+
+
+
+
+ org.testcontainers
+ postgresql
+ ${testcontainers.postgresql.version}
+ test
+
+
+
+ org.postgresql
+ postgresql
+
+
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.google.guava
+ guava
+ ${guava.version}
+
+
+
+
+ com.baeldung.boot.Application
+ 1.10.6
+ 42.2.5
+ 21.0
+
+
+
\ No newline at end of file
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/Application.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/Application.java
new file mode 100644
index 0000000000..3ea3d113da
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/Application.java
@@ -0,0 +1,13 @@
+package com.baeldung;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/config/StudentJpaConfig.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/config/StudentJpaConfig.java
new file mode 100644
index 0000000000..08f37ea806
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/config/StudentJpaConfig.java
@@ -0,0 +1,68 @@
+package com.baeldung.config;
+
+import java.util.Properties;
+
+import javax.persistence.EntityManagerFactory;
+import javax.sql.DataSource;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.core.env.Environment;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+@Configuration
+@EnableJpaRepositories(basePackages = "com.baeldung.inmemory.persistence.dao")
+@PropertySource("persistence-student.properties")
+@EnableTransactionManagement
+public class StudentJpaConfig {
+
+ @Autowired
+ private Environment env;
+
+ @Bean
+ public DataSource dataSource() {
+ final DriverManagerDataSource dataSource = new DriverManagerDataSource();
+ dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
+ dataSource.setUrl(env.getProperty("jdbc.url"));
+ dataSource.setUsername(env.getProperty("jdbc.user"));
+ dataSource.setPassword(env.getProperty("jdbc.pass"));
+
+ return dataSource;
+ }
+
+ @Bean
+ public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
+ final LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
+ em.setDataSource(dataSource());
+ em.setPackagesToScan(new String[] { "com.baeldung.inmemory.persistence.model" });
+ em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
+ em.setJpaProperties(additionalProperties());
+ return em;
+ }
+
+ @Bean
+ JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
+ JpaTransactionManager transactionManager = new JpaTransactionManager();
+ transactionManager.setEntityManagerFactory(entityManagerFactory);
+ return transactionManager;
+ }
+
+ final Properties additionalProperties() {
+ final Properties hibernateProperties = new Properties();
+
+ hibernateProperties.setProperty("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
+ hibernateProperties.setProperty("hibernate.dialect", env.getProperty("hibernate.dialect"));
+ hibernateProperties.setProperty("hibernate.show_sql", env.getProperty("hibernate.show_sql"));
+ hibernateProperties.setProperty("hibernate.cache.use_second_level_cache", env.getProperty("hibernate.cache.use_second_level_cache"));
+ hibernateProperties.setProperty("hibernate.cache.use_query_cache", env.getProperty("hibernate.cache.use_query_cache"));
+
+ return hibernateProperties;
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/entity/Customer.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/entity/Customer.java
new file mode 100644
index 0000000000..efcae73853
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/entity/Customer.java
@@ -0,0 +1,37 @@
+package com.baeldung.entity;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+
+@Entity
+public class Customer {
+
+ @Id
+ @GeneratedValue
+ private long id;
+ private String name;
+ private String email;
+
+ public Customer(String name, String email) {
+ this.name = name;
+ this.email = email;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/dao/ManyStudentRepository.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/dao/ManyStudentRepository.java
new file mode 100644
index 0000000000..a2aa0c5780
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/dao/ManyStudentRepository.java
@@ -0,0 +1,10 @@
+package com.baeldung.inmemory.persistence.dao;
+
+import com.baeldung.inmemory.persistence.model.ManyStudent;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface ManyStudentRepository extends JpaRepository {
+ List findByManyTags_Name(String name);
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/dao/ManyTagRepository.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/dao/ManyTagRepository.java
new file mode 100644
index 0000000000..63337f3cb9
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/dao/ManyTagRepository.java
@@ -0,0 +1,7 @@
+package com.baeldung.inmemory.persistence.dao;
+
+import com.baeldung.inmemory.persistence.model.ManyTag;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ManyTagRepository extends JpaRepository {
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/dao/StudentRepository.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/dao/StudentRepository.java
new file mode 100644
index 0000000000..8ac91fbf0c
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/dao/StudentRepository.java
@@ -0,0 +1,24 @@
+package com.baeldung.inmemory.persistence.dao;
+
+import com.baeldung.inmemory.persistence.model.Student;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+
+public interface StudentRepository extends JpaRepository {
+
+ @Query("SELECT s FROM Student s JOIN s.tags t WHERE t = LOWER(:tag)")
+ List retrieveByTag(@Param("tag") String tag);
+
+ @Query("SELECT s FROM Student s JOIN s.tags t WHERE s.name = LOWER(:name) AND t = LOWER(:tag)")
+ List retrieveByNameFilterByTag(@Param("name") String name, @Param("tag") String tag);
+
+ @Query("SELECT s FROM Student s JOIN s.skillTags t WHERE t.name = LOWER(:tagName) AND t.value > :tagValue")
+ List retrieveByNameFilterByMinimumSkillTag(@Param("tagName") String tagName, @Param("tagValue") int tagValue);
+
+ @Query("SELECT s FROM Student s JOIN s.kvTags t WHERE t.key = LOWER(:key)")
+ List retrieveByKeyTag(@Param("key") String key);
+
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/KVTag.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/KVTag.java
new file mode 100644
index 0000000000..1fc186f4ce
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/KVTag.java
@@ -0,0 +1,34 @@
+package com.baeldung.inmemory.persistence.model;
+
+import javax.persistence.Embeddable;
+
+@Embeddable
+public class KVTag {
+ private String key;
+ private String value;
+
+ public KVTag() {
+ }
+
+ public KVTag(String key, String value) {
+ super();
+ this.key = key;
+ this.value = value;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/LocationTag.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/LocationTag.java
new file mode 100644
index 0000000000..b12ad9fbd1
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/LocationTag.java
@@ -0,0 +1,40 @@
+package com.baeldung.inmemory.persistence.model;
+
+import javax.persistence.Embeddable;
+
+@Embeddable
+public class LocationTag {
+ private String name;
+ private int xPos;
+ private int yPos;
+
+ public LocationTag() {
+ }
+
+ public LocationTag(String name, int xPos, int yPos) {
+ super();
+ this.name = name;
+ this.xPos = xPos;
+ this.yPos = yPos;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getxPos() {
+ return xPos;
+ }
+
+ public void setxPos(int xPos) {
+ this.xPos = xPos;
+ }
+
+ public int getyPos() {
+ return yPos;
+ }
+
+ public void setyPos(int yPos) {
+ this.yPos = yPos;
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/ManyStudent.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/ManyStudent.java
new file mode 100644
index 0000000000..190740d582
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/ManyStudent.java
@@ -0,0 +1,33 @@
+package com.baeldung.inmemory.persistence.model;
+
+import javax.persistence.*;
+import java.util.HashSet;
+import java.util.Set;
+
+@Entity
+public class ManyStudent {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private int id;
+ private String name;
+
+ @ManyToMany(cascade = CascadeType.ALL)
+ @JoinTable(name = "manystudent_manytags", joinColumns = @JoinColumn(name = "manystudent_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "manytag_id", referencedColumnName = "id"))
+ private Set manyTags = new HashSet<>();
+
+ public ManyStudent() {
+ }
+
+ public ManyStudent(String name) {
+ this.name = name;
+ }
+
+ public Set getManyTags() {
+ return manyTags;
+ }
+
+ public void setManyTags(Set manyTags) {
+ this.manyTags.addAll(manyTags);
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/ManyTag.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/ManyTag.java
new file mode 100644
index 0000000000..5af898f7df
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/ManyTag.java
@@ -0,0 +1,40 @@
+package com.baeldung.inmemory.persistence.model;
+
+import javax.persistence.*;
+import java.util.HashSet;
+import java.util.Set;
+
+@Entity
+public class ManyTag {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private int id;
+ private String name;
+
+ @ManyToMany(mappedBy = "manyTags")
+ private Set students = new HashSet<>();
+
+ public ManyTag() {
+ }
+
+ public ManyTag(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Set getStudents() {
+ return students;
+ }
+
+ public void setStudents(Set students) {
+ this.students.addAll(students);
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/SkillTag.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/SkillTag.java
new file mode 100644
index 0000000000..738b5d0b36
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/SkillTag.java
@@ -0,0 +1,30 @@
+package com.baeldung.inmemory.persistence.model;
+
+import javax.persistence.Embeddable;
+
+@Embeddable
+public class SkillTag {
+ private String name;
+ private int value;
+
+ public SkillTag() {
+ }
+
+ public SkillTag(String name, int value) {
+ super();
+ this.name = name;
+ this.value = value;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+ public void setValue(int value) {
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/Student.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/Student.java
new file mode 100644
index 0000000000..f0e824e165
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/inmemory/persistence/model/Student.java
@@ -0,0 +1,74 @@
+package com.baeldung.inmemory.persistence.model;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.persistence.ElementCollection;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+
+@Entity
+public class Student {
+
+ @Id
+ private long id;
+ private String name;
+
+ @ElementCollection
+ private List tags = new ArrayList<>();
+
+ @ElementCollection
+ private List skillTags = new ArrayList<>();
+
+ @ElementCollection
+ private List kvTags = new ArrayList<>();
+
+ public Student() {
+ }
+
+ public Student(long id, String name) {
+ super();
+ this.id = id;
+ this.name = name;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public List getTags() {
+ return tags;
+ }
+
+ public void setTags(List tags) {
+ this.tags.addAll(tags);
+ }
+
+ public List getSkillTags() {
+ return skillTags;
+ }
+
+ public void setSkillTags(List skillTags) {
+ this.skillTags.addAll(skillTags);
+ }
+
+ public List getKVTags() {
+ return this.kvTags;
+ }
+
+ public void setKVTags(List kvTags) {
+ this.kvTags.addAll(kvTags);
+ }
+
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/model/Address.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/model/Address.java
new file mode 100644
index 0000000000..0c5a3eac60
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/model/Address.java
@@ -0,0 +1,57 @@
+package com.baeldung.projection.model;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.OneToOne;
+
+@Entity
+public class Address {
+ @Id
+ private Long id;
+ @OneToOne
+ private Person person;
+ private String state;
+ private String city;
+ private String street;
+ private String zipCode;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+ public void setState(String state) {
+ this.state = state;
+ }
+
+ public String getCity() {
+ return city;
+ }
+
+ public void setCity(String city) {
+ this.city = city;
+ }
+
+ public String getStreet() {
+ return street;
+ }
+
+ public void setStreet(String street) {
+ this.street = street;
+ }
+
+ public String getZipCode() {
+ return zipCode;
+ }
+
+ public void setZipCode(String zipCode) {
+ this.zipCode = zipCode;
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/model/Person.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/model/Person.java
new file mode 100644
index 0000000000..d18bd1c72d
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/model/Person.java
@@ -0,0 +1,47 @@
+package com.baeldung.projection.model;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.OneToOne;
+
+@Entity
+public class Person {
+ @Id
+ private Long id;
+ private String firstName;
+ private String lastName;
+ @OneToOne(mappedBy = "person")
+ private Address address;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public Address getAddress() {
+ return address;
+ }
+
+ public void setAddress(Address address) {
+ this.address = address;
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/repository/AddressRepository.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/repository/AddressRepository.java
new file mode 100644
index 0000000000..c1053f4867
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/repository/AddressRepository.java
@@ -0,0 +1,11 @@
+package com.baeldung.projection.repository;
+
+import com.baeldung.projection.view.AddressView;
+import com.baeldung.projection.model.Address;
+import org.springframework.data.repository.Repository;
+
+import java.util.List;
+
+public interface AddressRepository extends Repository {
+ List getAddressByState(String state);
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/repository/PersonRepository.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/repository/PersonRepository.java
new file mode 100644
index 0000000000..64bc7471e6
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/repository/PersonRepository.java
@@ -0,0 +1,14 @@
+package com.baeldung.projection.repository;
+
+import com.baeldung.projection.model.Person;
+import com.baeldung.projection.view.PersonDto;
+import com.baeldung.projection.view.PersonView;
+import org.springframework.data.repository.Repository;
+
+public interface PersonRepository extends Repository {
+ PersonView findByLastName(String lastName);
+
+ PersonDto findByFirstName(String firstName);
+
+ T findByLastName(String lastName, Class type);
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/view/AddressView.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/view/AddressView.java
new file mode 100644
index 0000000000..7a24a1e9b9
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/view/AddressView.java
@@ -0,0 +1,7 @@
+package com.baeldung.projection.view;
+
+public interface AddressView {
+ String getZipCode();
+
+ PersonView getPerson();
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/view/PersonDto.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/view/PersonDto.java
new file mode 100644
index 0000000000..1fd924450b
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/view/PersonDto.java
@@ -0,0 +1,34 @@
+package com.baeldung.projection.view;
+
+import java.util.Objects;
+
+public class PersonDto {
+ private final String firstName;
+ private final String lastName;
+
+ public PersonDto(String firstName, String lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ PersonDto personDto = (PersonDto) o;
+ return Objects.equals(firstName, personDto.firstName) && Objects.equals(lastName, personDto.lastName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(firstName, lastName);
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/view/PersonView.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/view/PersonView.java
new file mode 100644
index 0000000000..36777ec26f
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/projection/view/PersonView.java
@@ -0,0 +1,12 @@
+package com.baeldung.projection.view;
+
+import org.springframework.beans.factory.annotation.Value;
+
+public interface PersonView {
+ String getFirstName();
+
+ String getLastName();
+
+ @Value("#{target.firstName + ' ' + target.lastName}")
+ String getFullName();
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/repository/CustomerRepository.java b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/repository/CustomerRepository.java
new file mode 100644
index 0000000000..65b22bbd84
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/java/com/baeldung/repository/CustomerRepository.java
@@ -0,0 +1,19 @@
+package com.baeldung.repository;
+
+import com.baeldung.entity.Customer;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+
+public interface CustomerRepository extends JpaRepository {
+
+ List findByName(String name);
+
+ List findByNameAndEmail(String name, String email);
+
+ @Query("SELECT c FROM Customer c WHERE (:name is null or c.name = :name) and (:email is null or c.email = :email)")
+ List findCustomerByNameAndEmail(@Param("name") String name, @Param("email") String email);
+
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/main/resources/persistence-student.properties b/persistence-modules/spring-data-jpa-filtering/src/main/resources/persistence-student.properties
new file mode 100644
index 0000000000..d4c82420de
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/main/resources/persistence-student.properties
@@ -0,0 +1,11 @@
+jdbc.driverClassName=com.mysql.cj.jdbc.Driver
+jdbc.url=jdbc:mysql://localhost:3306/myDb
+jdbc.user=tutorialuser
+jdbc.pass=tutorialpass
+
+hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
+hibernate.show_sql=true
+hibernate.hbm2ddl.auto=create-drop
+
+hibernate.cache.use_second_level_cache=false
+hibernate.cache.use_query_cache=false
\ No newline at end of file
diff --git a/persistence-modules/spring-data-jpa-filtering/src/test/java/com/baeldung/persistence/repository/AdvancedTaggingIntegrationTest.java b/persistence-modules/spring-data-jpa-filtering/src/test/java/com/baeldung/persistence/repository/AdvancedTaggingIntegrationTest.java
new file mode 100644
index 0000000000..e98c1fa4a6
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/test/java/com/baeldung/persistence/repository/AdvancedTaggingIntegrationTest.java
@@ -0,0 +1,83 @@
+package com.baeldung.persistence.repository;
+
+import com.baeldung.config.StudentJpaConfig;
+import com.baeldung.inmemory.persistence.dao.ManyStudentRepository;
+import com.baeldung.inmemory.persistence.dao.ManyTagRepository;
+import com.baeldung.inmemory.persistence.dao.StudentRepository;
+import com.baeldung.inmemory.persistence.model.ManyStudent;
+import com.baeldung.inmemory.persistence.model.ManyTag;
+import com.baeldung.inmemory.persistence.model.SkillTag;
+import com.baeldung.inmemory.persistence.model.Student;
+import com.baeldung.inmemory.persistence.model.KVTag;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.support.AnnotationConfigContextLoader;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes = { StudentJpaConfig.class }, loader = AnnotationConfigContextLoader.class)
+@Transactional
+@DirtiesContext
+public class AdvancedTaggingIntegrationTest {
+ @Resource
+ private StudentRepository studentRepository;
+
+ @Resource
+ private ManyStudentRepository manyStudentRepository;
+
+ @Resource
+ private ManyTagRepository manyTagRepository;
+
+ @Test
+ public void givenStudentWithSkillTags_whenSave_thenGetByNameAndSkillTag() {
+ Student student = new Student(1, "Will");
+ SkillTag skill1 = new SkillTag("java", 5);
+ student.setSkillTags(Arrays.asList(skill1));
+ studentRepository.save(student);
+
+ Student student2 = new Student(2, "Joe");
+ SkillTag skill2 = new SkillTag("java", 1);
+ student2.setSkillTags(Arrays.asList(skill2));
+ studentRepository.save(student2);
+
+ List students = studentRepository.retrieveByNameFilterByMinimumSkillTag("java", 3);
+ assertEquals("size incorrect", 1, students.size());
+ }
+
+ @Test
+ public void givenStudentWithKVTags_whenSave_thenGetByTagOk() {
+ Student student = new Student(0, "John");
+ student.setKVTags(Arrays.asList(new KVTag("department", "computer science")));
+ studentRepository.save(student);
+
+ Student student2 = new Student(1, "James");
+ student2.setKVTags(Arrays.asList(new KVTag("department", "humanities")));
+ studentRepository.save(student2);
+
+ List students = studentRepository.retrieveByKeyTag("department");
+ assertEquals("size incorrect", 2, students.size());
+ }
+
+ @Test
+ public void givenStudentWithManyTags_whenSave_theyGetByTagOk() {
+ ManyTag tag = new ManyTag("full time");
+ manyTagRepository.save(tag);
+
+ ManyStudent student = new ManyStudent("John");
+ student.setManyTags(Collections.singleton(tag));
+ manyStudentRepository.save(student);
+
+ List students = manyStudentRepository.findByManyTags_Name("full time");
+ assertEquals("size incorrect", 1, students.size());
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/test/java/com/baeldung/persistence/repository/InMemoryDBIntegrationTest.java b/persistence-modules/spring-data-jpa-filtering/src/test/java/com/baeldung/persistence/repository/InMemoryDBIntegrationTest.java
new file mode 100644
index 0000000000..d8d867792e
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/test/java/com/baeldung/persistence/repository/InMemoryDBIntegrationTest.java
@@ -0,0 +1,84 @@
+package com.baeldung.persistence.repository;
+
+import com.baeldung.config.StudentJpaConfig;
+import com.baeldung.inmemory.persistence.dao.StudentRepository;
+import com.baeldung.inmemory.persistence.model.Student;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.support.AnnotationConfigContextLoader;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes = { StudentJpaConfig.class }, loader = AnnotationConfigContextLoader.class)
+@Transactional
+@DirtiesContext
+public class InMemoryDBIntegrationTest {
+
+ @Resource
+ private StudentRepository studentRepository;
+
+ private static final long ID = 1;
+ private static final String NAME = "john";
+
+ @Test
+ public void givenStudent_whenSave_thenGetOk() {
+ Student student = new Student(ID, NAME);
+ studentRepository.save(student);
+
+ Student student2 = studentRepository.findById(ID).get();
+ assertEquals("name incorrect", NAME, student2.getName());
+ }
+
+ @Test
+ public void givenStudentWithTags_whenSave_thenGetByTagOk() {
+ Student student = new Student(ID, NAME);
+ student.setTags(Arrays.asList("full time", "computer science"));
+ studentRepository.save(student);
+
+ Student student2 = studentRepository.retrieveByTag("full time").get(0);
+ assertEquals("name incorrect", NAME, student2.getName());
+ }
+
+ @Test
+ public void givenMultipleStudentsWithTags_whenSave_thenGetByTagReturnsCorrectCount() {
+ Student student = new Student(0, "Larry");
+ student.setTags(Arrays.asList("full time", "computer science"));
+ studentRepository.save(student);
+
+ Student student2 = new Student(1, "Curly");
+ student2.setTags(Arrays.asList("part time", "rocket science"));
+ studentRepository.save(student2);
+
+ Student student3 = new Student(2, "Moe");
+ student3.setTags(Arrays.asList("full time", "philosophy"));
+ studentRepository.save(student3);
+
+ Student student4 = new Student(3, "Shemp");
+ student4.setTags(Arrays.asList("part time", "mathematics"));
+ studentRepository.save(student4);
+
+ List students = studentRepository.retrieveByTag("full time");
+ assertEquals("size incorrect", 2, students.size());
+ }
+
+ @Test
+ public void givenStudentWithTags_whenSave_thenGetByNameAndTagOk() {
+ Student student = new Student(ID, NAME);
+ student.setTags(Arrays.asList("full time", "computer science"));
+ studentRepository.save(student);
+
+ Student student2 = studentRepository.retrieveByNameFilterByTag("John", "full time").get(0);
+ assertEquals("name incorrect", NAME, student2.getName());
+ }
+
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/test/java/com/baeldung/projection/JpaProjectionIntegrationTest.java b/persistence-modules/spring-data-jpa-filtering/src/test/java/com/baeldung/projection/JpaProjectionIntegrationTest.java
new file mode 100644
index 0000000000..96eaf4ed07
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/test/java/com/baeldung/projection/JpaProjectionIntegrationTest.java
@@ -0,0 +1,63 @@
+package com.baeldung.projection;
+
+import com.baeldung.projection.model.Person;
+import com.baeldung.projection.repository.AddressRepository;
+import com.baeldung.projection.repository.PersonRepository;
+import com.baeldung.projection.view.AddressView;
+import com.baeldung.projection.view.PersonDto;
+import com.baeldung.projection.view.PersonView;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
+
+@DataJpaTest
+@RunWith(SpringRunner.class)
+@Sql(scripts = "/projection-insert-data.sql")
+@Sql(scripts = "/projection-clean-up-data.sql", executionPhase = AFTER_TEST_METHOD)
+public class JpaProjectionIntegrationTest {
+ @Autowired
+ private AddressRepository addressRepository;
+
+ @Autowired
+ private PersonRepository personRepository;
+
+ @Test
+ public void whenUsingClosedProjections_thenViewWithRequiredPropertiesIsReturned() {
+ AddressView addressView = addressRepository.getAddressByState("CA").get(0);
+ assertThat(addressView.getZipCode()).isEqualTo("90001");
+
+ PersonView personView = addressView.getPerson();
+ assertThat(personView.getFirstName()).isEqualTo("John");
+ assertThat(personView.getLastName()).isEqualTo("Doe");
+ }
+
+ @Test
+ public void whenUsingOpenProjections_thenViewWithRequiredPropertiesIsReturned() {
+ PersonView personView = personRepository.findByLastName("Doe");
+ assertThat(personView.getFullName()).isEqualTo("John Doe");
+ }
+
+ @Test
+ public void whenUsingClassBasedProjections_thenDtoWithRequiredPropertiesIsReturned() {
+ PersonDto personDto = personRepository.findByFirstName("John");
+ assertThat(personDto.getFirstName()).isEqualTo("John");
+ assertThat(personDto.getLastName()).isEqualTo("Doe");
+ }
+
+ @Test
+ public void whenUsingDynamicProjections_thenObjectWithRequiredPropertiesIsReturned() {
+ Person person = personRepository.findByLastName("Doe", Person.class);
+ PersonView personView = personRepository.findByLastName("Doe", PersonView.class);
+ PersonDto personDto = personRepository.findByLastName("Doe", PersonDto.class);
+
+ assertThat(person.getFirstName()).isEqualTo("John");
+ assertThat(personView.getFirstName()).isEqualTo("John");
+ assertThat(personDto.getFirstName()).isEqualTo("John");
+ }
+}
diff --git a/persistence-modules/spring-data-jpa-filtering/src/test/java/com/baeldung/repository/CustomerRepositoryIntegrationTest.java b/persistence-modules/spring-data-jpa-filtering/src/test/java/com/baeldung/repository/CustomerRepositoryIntegrationTest.java
new file mode 100644
index 0000000000..5d6457ce30
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/test/java/com/baeldung/repository/CustomerRepositoryIntegrationTest.java
@@ -0,0 +1,64 @@
+package com.baeldung.repository;
+
+import com.baeldung.entity.Customer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+@DataJpaTest
+@RunWith(SpringRunner.class)
+public class CustomerRepositoryIntegrationTest {
+
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ @Autowired
+ private CustomerRepository repository;
+
+ @Before
+ public void before() {
+ entityManager.persist(new Customer("A", "A@example.com"));
+ entityManager.persist(new Customer("D", null));
+ entityManager.persist(new Customer("D", "D@example.com"));
+ }
+
+ @Test
+ public void givenQueryMethod_whenEmailIsNull_thenFoundByNullEmail() {
+ List customers = repository.findByNameAndEmail("D", null);
+
+ assertEquals(1, customers.size());
+ Customer actual = customers.get(0);
+
+ assertEquals(null, actual.getEmail());
+ assertEquals("D", actual.getName());
+ }
+
+ @Test
+ public void givenQueryMethod_whenEmailIsAbsent_thenIgnoreEmail() {
+ List customers = repository.findByName("D");
+
+ assertEquals(2, customers.size());
+ }
+
+ @Test
+ public void givenQueryAnnotation_whenEmailIsNull_thenIgnoreEmail() {
+ List customers = repository.findCustomerByNameAndEmail("D", null);
+
+ assertEquals(2, customers.size());
+ }
+
+ @After
+ public void cleanUp() {
+ repository.deleteAll();
+ }
+}
\ No newline at end of file
diff --git a/persistence-modules/spring-data-jpa-filtering/src/test/resources/persistence-student.properties b/persistence-modules/spring-data-jpa-filtering/src/test/resources/persistence-student.properties
new file mode 100644
index 0000000000..3b6b580630
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/test/resources/persistence-student.properties
@@ -0,0 +1,9 @@
+jdbc.driverClassName=org.h2.Driver
+jdbc.url=jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1
+
+hibernate.dialect=org.hibernate.dialect.H2Dialect
+hibernate.show_sql=true
+hibernate.hbm2ddl.auto=create
+
+hibernate.cache.use_second_level_cache=false
+hibernate.cache.use_query_cache=false
\ No newline at end of file
diff --git a/persistence-modules/spring-data-jpa-filtering/src/test/resources/projection-clean-up-data.sql b/persistence-modules/spring-data-jpa-filtering/src/test/resources/projection-clean-up-data.sql
new file mode 100644
index 0000000000..d34f6f0636
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/test/resources/projection-clean-up-data.sql
@@ -0,0 +1,2 @@
+DELETE FROM address;
+DELETE FROM person;
\ No newline at end of file
diff --git a/persistence-modules/spring-data-jpa-filtering/src/test/resources/projection-insert-data.sql b/persistence-modules/spring-data-jpa-filtering/src/test/resources/projection-insert-data.sql
new file mode 100644
index 0000000000..544dcc4b88
--- /dev/null
+++ b/persistence-modules/spring-data-jpa-filtering/src/test/resources/projection-insert-data.sql
@@ -0,0 +1,2 @@
+INSERT INTO person(id,first_name,last_name) VALUES (1,'John','Doe');
+INSERT INTO address(id,person_id,state,city,street,zip_code) VALUES (1,1,'CA', 'Los Angeles', 'Standford Ave', '90001');
\ No newline at end of file