From 5c01c4413085c37cb1f66ff9dce6bcbb2d2245e9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 28 Jul 2017 11:27:02 +0200 Subject: [PATCH] #298 - Add JPA example for composable repositories. --- .../CompositeRepositoryConfig.java | 26 ++++ .../springdata/jpa/compositions/Contact.java | 34 +++++ .../jpa/compositions/ContactRepository.java | 34 +++++ .../compositions/ContactRepositoryImpl.java | 47 +++++++ .../springdata/jpa/compositions/Employee.java | 29 ++++ .../jpa/compositions/EmployeeRepository.java | 38 +++++ .../compositions/EmployeeRepositoryImpl.java | 60 ++++++++ .../compositions/FlushOnSaveRepository.java | 42 ++++++ .../FlushOnSaveRepositoryImpl.java | 95 +++++++++++++ .../springdata/jpa/compositions/User.java | 43 ++++++ .../jpa/compositions/UserRepository.java | 27 ++++ .../compositions/ComposedRepositoryTests.java | 132 ++++++++++++++++++ 12 files changed, 607 insertions(+) create mode 100644 jpa/example/src/main/java/example/springdata/jpa/compositions/CompositeRepositoryConfig.java create mode 100644 jpa/example/src/main/java/example/springdata/jpa/compositions/Contact.java create mode 100644 jpa/example/src/main/java/example/springdata/jpa/compositions/ContactRepository.java create mode 100644 jpa/example/src/main/java/example/springdata/jpa/compositions/ContactRepositoryImpl.java create mode 100644 jpa/example/src/main/java/example/springdata/jpa/compositions/Employee.java create mode 100644 jpa/example/src/main/java/example/springdata/jpa/compositions/EmployeeRepository.java create mode 100644 jpa/example/src/main/java/example/springdata/jpa/compositions/EmployeeRepositoryImpl.java create mode 100644 jpa/example/src/main/java/example/springdata/jpa/compositions/FlushOnSaveRepository.java create mode 100644 jpa/example/src/main/java/example/springdata/jpa/compositions/FlushOnSaveRepositoryImpl.java create mode 100644 jpa/example/src/main/java/example/springdata/jpa/compositions/User.java create mode 100644 jpa/example/src/main/java/example/springdata/jpa/compositions/UserRepository.java create mode 100644 jpa/example/src/test/java/example/springdata/jpa/compositions/ComposedRepositoryTests.java diff --git a/jpa/example/src/main/java/example/springdata/jpa/compositions/CompositeRepositoryConfig.java b/jpa/example/src/main/java/example/springdata/jpa/compositions/CompositeRepositoryConfig.java new file mode 100644 index 00000000..ba28eac7 --- /dev/null +++ b/jpa/example/src/main/java/example/springdata/jpa/compositions/CompositeRepositoryConfig.java @@ -0,0 +1,26 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.compositions; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Sample configuration to bootstrap Spring Data JPA through JavaConfig + * + * @author Mark Paluch + */ +@SpringBootApplication +class CompositeRepositoryConfig {} diff --git a/jpa/example/src/main/java/example/springdata/jpa/compositions/Contact.java b/jpa/example/src/main/java/example/springdata/jpa/compositions/Contact.java new file mode 100644 index 00000000..f8ab9428 --- /dev/null +++ b/jpa/example/src/main/java/example/springdata/jpa/compositions/Contact.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.compositions; + +/** + * Entity aspect containing firstname and lastname properties. + * + * @author Mark Paluch + */ +public interface Contact { + + /** + * @return the first name. + */ + String getFirstname(); + + /** + * @return the last name. + */ + String getLastname(); +} diff --git a/jpa/example/src/main/java/example/springdata/jpa/compositions/ContactRepository.java b/jpa/example/src/main/java/example/springdata/jpa/compositions/ContactRepository.java new file mode 100644 index 00000000..7b2017b0 --- /dev/null +++ b/jpa/example/src/main/java/example/springdata/jpa/compositions/ContactRepository.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.compositions; + +import java.util.List; + +/** + * Repository fragment for {@link Contact}. + * + * @author Mark Paluch + */ +public interface ContactRepository { + + /** + * Find relatives of this {@link Contact}. + * + * @param contact must not be {@literal null}. + * @return {@link List} of relatives. + */ + List findRelatives(Contact contact); +} diff --git a/jpa/example/src/main/java/example/springdata/jpa/compositions/ContactRepositoryImpl.java b/jpa/example/src/main/java/example/springdata/jpa/compositions/ContactRepositoryImpl.java new file mode 100644 index 00000000..9b3d9bb4 --- /dev/null +++ b/jpa/example/src/main/java/example/springdata/jpa/compositions/ContactRepositoryImpl.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.compositions; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +import javax.persistence.EntityManager; + +/** + * {@link ContactRepository} fragment implementation. + * + * @author Mark Paluch + */ +@RequiredArgsConstructor +public class ContactRepositoryImpl implements ContactRepository { + + final @NonNull EntityManager entityManager; + + /* + * (non-Javadoc) + * @see example.springdata.jpa.compositions.ContactRepository#findRelatives(example.springdata.jpa.compositions.Contact) + */ + @Override + @SuppressWarnings("unchecked") + public List findRelatives(Contact contact) { + + return entityManager.createQuery("SELECT u FROM User u WHERE u.lastname = :lastname") // + .setParameter("lastname", contact.getLastname()) // + .getResultList(); + } +} diff --git a/jpa/example/src/main/java/example/springdata/jpa/compositions/Employee.java b/jpa/example/src/main/java/example/springdata/jpa/compositions/Employee.java new file mode 100644 index 00000000..b1ec27fd --- /dev/null +++ b/jpa/example/src/main/java/example/springdata/jpa/compositions/Employee.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.compositions; + +/** + * Aspect interface for employee entities. + * + * @author Mark Paluch + */ +public interface Employee { + + /** + * @return the {@link Employee manager} object for this {@link Employee}. + */ + Employee getManager(); +} diff --git a/jpa/example/src/main/java/example/springdata/jpa/compositions/EmployeeRepository.java b/jpa/example/src/main/java/example/springdata/jpa/compositions/EmployeeRepository.java new file mode 100644 index 00000000..489ed0ed --- /dev/null +++ b/jpa/example/src/main/java/example/springdata/jpa/compositions/EmployeeRepository.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.compositions; + +import java.util.List; + +/** + * Repository extension for {@link Employee} entities. + * + * @author Mark Paluch + */ +public interface EmployeeRepository { + + /** + * @param employee + * @return list of co-workers having the same {@link Employee#getManager() manager}. + */ + List findCoworkers(Employee employee); + + /** + * @param employee + * @return list of subordinates for this {@link Employee manager}. + */ + List findSubordinates(Employee manager); +} diff --git a/jpa/example/src/main/java/example/springdata/jpa/compositions/EmployeeRepositoryImpl.java b/jpa/example/src/main/java/example/springdata/jpa/compositions/EmployeeRepositoryImpl.java new file mode 100644 index 00000000..b3db1659 --- /dev/null +++ b/jpa/example/src/main/java/example/springdata/jpa/compositions/EmployeeRepositoryImpl.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.compositions; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +import javax.persistence.EntityManager; + +/** + * {@link EmployeeRepository} fragment implementation. + * + * @author Mark Paluch + */ +@RequiredArgsConstructor +public class EmployeeRepositoryImpl implements EmployeeRepository { + + final @NonNull EntityManager entityManager; + + /* + * (non-Javadoc) + * @see example.springdata.jpa.compositions.EmployeeRepository#findCoworkers(example.springdata.jpa.compositions.Employee) + */ + @Override + @SuppressWarnings("unchecked") + public List findCoworkers(Employee employee) { + + return entityManager.createQuery("SELECT u from User u where u.manager = :manager") // + .setParameter("manager", employee.getManager()) // + .getResultList(); + } + + /* + * (non-Javadoc) + * @see example.springdata.jpa.compositions.EmployeeRepository#findSubordinates(example.springdata.jpa.compositions.Employee) + */ + @Override + @SuppressWarnings("unchecked") + public List findSubordinates(Employee manager) { + + return entityManager.createQuery("SELECT u from User u where u.manager = :manager") // + .setParameter("manager", manager) // + .getResultList(); + } +} diff --git a/jpa/example/src/main/java/example/springdata/jpa/compositions/FlushOnSaveRepository.java b/jpa/example/src/main/java/example/springdata/jpa/compositions/FlushOnSaveRepository.java new file mode 100644 index 00000000..5d1f58d0 --- /dev/null +++ b/jpa/example/src/main/java/example/springdata/jpa/compositions/FlushOnSaveRepository.java @@ -0,0 +1,42 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.compositions; + +/** + * Generic repository extension for JPA repositories to flush after each save. + * + * @author Mark Paluch + */ +public interface FlushOnSaveRepository { + + /** + * Saves a given entity and flush immediately. Use the returned instance for further operations as the save operation + * might have changed the entity instance completely. + * + * @param entity must not be {@literal null}. + * @return the saved entity will never be {@literal null}. + */ + S save(S entity); + + /** + * Saves all given entities and flush after the save. + * + * @param entities must not be {@literal null}. + * @return the saved entities will never be {@literal null}. + * @throws IllegalArgumentException in case the given entity is {@literal null}. + */ + Iterable saveAll(Iterable entities); +} diff --git a/jpa/example/src/main/java/example/springdata/jpa/compositions/FlushOnSaveRepositoryImpl.java b/jpa/example/src/main/java/example/springdata/jpa/compositions/FlushOnSaveRepositoryImpl.java new file mode 100644 index 00000000..1cd77e4d --- /dev/null +++ b/jpa/example/src/main/java/example/springdata/jpa/compositions/FlushOnSaveRepositoryImpl.java @@ -0,0 +1,95 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.compositions; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import javax.persistence.EntityManager; + +import org.springframework.data.jpa.domain.AbstractPersistable; +import org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation; +import org.springframework.data.jpa.repository.support.JpaPersistableEntityInformation; +import org.springframework.data.repository.core.EntityInformation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.ClassUtils; + +/** + * Generic repository extension for JPA repositories to flush after each save. + * + * @author Mark Paluch + */ +@RequiredArgsConstructor +public class FlushOnSaveRepositoryImpl implements FlushOnSaveRepository { + + final @NonNull EntityManager entityManager; + + /* + * (non-Javadoc) + * @see example.springdata.jpa.compositions.FlushOnSaveRepository#save(java.lang.Object) + */ + @Transactional + @Override + public S save(S entity) { + + doSave(entity); + + entityManager.flush(); + + return entity; + } + + /** + * @param entity + */ + private void doSave(S entity) { + + EntityInformation entityInformation = getEntityInformation(entity); + + if (entityInformation.isNew(entity)) { + entityManager.persist(entity); + } else { + entityManager.merge(entity); + } + } + + /* + * (non-Javadoc) + * @see example.springdata.jpa.compositions.FlushOnSaveRepository#saveAll(java.lang.Iterable) + */ + @Transactional + @Override + public Iterable saveAll(Iterable entities) { + + entities.forEach(this::doSave); + + entityManager.flush(); + + return entities; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private EntityInformation getEntityInformation(S entity) { + + Class userClass = ClassUtils.getUserClass(entity.getClass()); + + if (entity instanceof AbstractPersistable) { + return new JpaPersistableEntityInformation(userClass, entityManager.getMetamodel()); + } + + return new JpaMetamodelEntityInformation(userClass, entityManager.getMetamodel()); + } +} diff --git a/jpa/example/src/main/java/example/springdata/jpa/compositions/User.java b/jpa/example/src/main/java/example/springdata/jpa/compositions/User.java new file mode 100644 index 00000000..23a1d4b9 --- /dev/null +++ b/jpa/example/src/main/java/example/springdata/jpa/compositions/User.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.compositions; + +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.ManyToOne; + +import org.springframework.data.jpa.domain.AbstractPersistable; + +/** + * Sample user class. + * + * @author Mark Paluch + */ +@Entity +@Getter +@Setter +public class User extends AbstractPersistable implements Contact, Employee { + + @Column(unique = true) private String username; + + private String firstname; + private String lastname; + + @ManyToOne private User manager; +} diff --git a/jpa/example/src/main/java/example/springdata/jpa/compositions/UserRepository.java b/jpa/example/src/main/java/example/springdata/jpa/compositions/UserRepository.java new file mode 100644 index 00000000..8b4f7989 --- /dev/null +++ b/jpa/example/src/main/java/example/springdata/jpa/compositions/UserRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.compositions; + +import org.springframework.data.repository.CrudRepository; + +/** + * Composite repository implementing a default {@link CrudRepository} extended with {@link ContactRepository}, + * {@link EmployeeRepository}, and {@link FlushOnSaveRepository} fragments. + * + * @author Mark Paluch + */ +public interface UserRepository + extends CrudRepository, ContactRepository, EmployeeRepository, FlushOnSaveRepository {} diff --git a/jpa/example/src/test/java/example/springdata/jpa/compositions/ComposedRepositoryTests.java b/jpa/example/src/test/java/example/springdata/jpa/compositions/ComposedRepositoryTests.java new file mode 100644 index 00000000..74846ea2 --- /dev/null +++ b/jpa/example/src/test/java/example/springdata/jpa/compositions/ComposedRepositoryTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.compositions; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration test showing the usage of a composite repository via {@link UserRepository}. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@Transactional +@SpringBootTest +public class ComposedRepositoryTests { + + @Autowired UserRepository repository; + + /** + * Tests inserting a user and asserts it can be loaded again. + */ + @Test + public void testInsert() { + + User user = new User(); + user.setUsername("username"); + + user = repository.save(user); + + assertThat(repository.findById(user.getId())).hasValue(user); + } + + /** + * Testing {@link ContactRepository} fragment. + */ + @Test + public void testContactRepository() { + + User walter = new User(); + walter.setUsername("heisenberg"); + walter.setFirstname("Walter"); + walter.setLastname("White"); + + User walterJr = new User(); + walterJr.setUsername("flynn"); + walterJr.setFirstname("Walter Jr."); + walterJr.setLastname("White"); + + repository.saveAll(Arrays.asList(walter, walterJr)); + + assertThat(repository.findRelatives(walter)).contains(walterJr); + } + + /** + * Testing {@link EmployeeRepository} fragment. + */ + @Test + public void testFindCoworkers() { + + User gustavo = new User(); + gustavo.setUsername("pollosh"); + gustavo.setFirstname("Gustavo"); + gustavo.setLastname("Fring"); + + User walter = new User(); + walter.setUsername("heisenberg"); + walter.setFirstname("Walter"); + walter.setLastname("White"); + walter.setManager(gustavo); + + User jesse = new User(); + jesse.setUsername("capncook"); + jesse.setFirstname("Jesse"); + jesse.setLastname("Pinkman"); + jesse.setManager(gustavo); + + repository.saveAll(Arrays.asList(gustavo, walter, jesse)); + + assertThat(repository.findCoworkers(walter)).contains(jesse); + } + + /** + * Testing {@link EmployeeRepository} fragment. + */ + @Test + public void testFindSubordinates() { + + User gustavo = new User(); + gustavo.setUsername("pollosh"); + gustavo.setFirstname("Gustavo"); + gustavo.setLastname("Fring"); + + User walter = new User(); + walter.setUsername("heisenberg"); + walter.setFirstname("Walter"); + walter.setLastname("White"); + walter.setManager(gustavo); + + User jesse = new User(); + jesse.setUsername("capncook"); + jesse.setFirstname("Jesse"); + jesse.setLastname("Pinkman"); + jesse.setManager(gustavo); + + repository.saveAll(Arrays.asList(gustavo, walter, jesse)); + + assertThat(repository.findSubordinates(gustavo)).contains(walter, jesse); + assertThat(repository.findSubordinates(walter)).isEmpty(); + } +}