#298 - Add JPA example for composable repositories.

This commit is contained in:
Mark Paluch
2017-07-28 11:27:02 +02:00
committed by Oliver Gierke
parent a0775f774b
commit 5c01c44130
12 changed files with 607 additions and 0 deletions

View File

@@ -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 {}

View File

@@ -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();
}

View File

@@ -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<Contact> findRelatives(Contact contact);
}

View File

@@ -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<Contact> findRelatives(Contact contact) {
return entityManager.createQuery("SELECT u FROM User u WHERE u.lastname = :lastname") //
.setParameter("lastname", contact.getLastname()) //
.getResultList();
}
}

View File

@@ -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();
}

View File

@@ -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<Employee> findCoworkers(Employee employee);
/**
* @param employee
* @return list of subordinates for this {@link Employee manager}.
*/
List<Employee> findSubordinates(Employee manager);
}

View File

@@ -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<Employee> 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<Employee> findSubordinates(Employee manager) {
return entityManager.createQuery("SELECT u from User u where u.manager = :manager") //
.setParameter("manager", manager) //
.getResultList();
}
}

View File

@@ -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<T> {
/**
* 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 extends T> 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}.
*/
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
}

View File

@@ -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<T> implements FlushOnSaveRepository<T> {
final @NonNull EntityManager entityManager;
/*
* (non-Javadoc)
* @see example.springdata.jpa.compositions.FlushOnSaveRepository#save(java.lang.Object)
*/
@Transactional
@Override
public <S extends T> S save(S entity) {
doSave(entity);
entityManager.flush();
return entity;
}
/**
* @param entity
*/
private <S extends T> void doSave(S entity) {
EntityInformation<Object, S> 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 <S extends T> Iterable<S> saveAll(Iterable<S> entities) {
entities.forEach(this::doSave);
entityManager.flush();
return entities;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private <S extends T> EntityInformation<Object, S> 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());
}
}

View File

@@ -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<Long> implements Contact, Employee {
@Column(unique = true) private String username;
private String firstname;
private String lastname;
@ManyToOne private User manager;
}

View File

@@ -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<User, Long>, ContactRepository, EmployeeRepository, FlushOnSaveRepository<User> {}

View File

@@ -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();
}
}