Это часть проекта Студент . Другие должности: клиент Webservice с Джерси , сервер Webservice с Джерси , бизнес-уровень , постоянство с данными Spring , тестовые данные интеграции интеграции и интеграция с веб-сервисом .
Мы рассмотрели основные операции CRUD, но это не очень далеко заходит. Spring Data позволяет легко включать базовые поиски, но важно иметь другие стандартные опции. Одним из наиболее важных является запрос критериев JPA.
Хорошим введением в этот материал является учебник Spring Data JPA — JPA Criteria Queries [http://www.petrikainulainen.net].
Проектные решения
Критерии JPA — я использую критерии поиска JPA вместо querydsl. Я вернусь к querydsl позже.
Ограничения
Нарушение инкапсуляции — этот дизайн требует решения архитектурной задачи, заключающейся в том, чтобы каждый слой не знал о деталях реализации других слоев. Это очень небольшое нарушение — только спецификации JPA — и нам все еще приходится разбираться с нумерацией страниц. Сложите их вместе, и я чувствую, что преждевременная оптимизация беспокоиться об этом слишком много сейчас.
Веб-сервисы — клиент и сервер веб-сервиса не обновляются. Опять же, нам по-прежнему приходится разбираться с нумерацией страниц, и все, что мы сейчас делаем, все равно придется изменить.
Рефакторинг предыдущей работы
У меня есть три изменения в предыдущей работе.
findCoursesByTestRun ()
Я определил метод:
1
|
List findCoursesByTestRun(TestRun testRun); |
в репозитории курса. Это не имеет ожидаемого эффекта. Что мне было нужно
1
|
List findCoursesByTestRun_Uuid(String uuid); |
с соответствующими изменениями в коде вызова. Или вы можете просто использовать запрос критериев JPA, описанный ниже.
FinderService и ManagerService
Это происходит из исследований на сайте Jumpstart . Автор разбивает стандартный интерфейс сервиса на две части:
- FinderService — операции только для чтения (поиски)
- ManagerService — операции чтения-записи (создание, обновление, удаление)
Это имеет большой смысл, например, будет намного проще добавить поведение через АОП, когда мы сможем работать на уровне класса и метода. Я внес соответствующие изменения в существующий код.
FindBugs
Я исправил ряд проблем, обнаруженных FindBugs.
Метаданные
Мы начнем с предоставления доступа к метаданным доступа к нашим постоянным объектам. Это позволяет нам создавать запросы, которые могут быть оптимизированы реализацией JPA.
1
2
3
4
5
6
7
|
import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.StaticMetamodel; @StaticMetamodel (Course. class ) public class Course_ { public static volatile SingularAttribute<Course, TestRun> testRun; } |
и
1
2
3
4
|
@StaticMetamodel (TestRun. class ) public class TestRun_ { public static volatile SingularAttribute<TestRun, String> uuid; } |
Имя класса требуется из-за соглашения по конфигурации.
- Для обсуждения этой функции см. Статические метаданные [jboss.org].
Характеристики
Теперь мы можем создать спецификацию запроса, используя доступные сейчас метаданные. Это немного более сложный запрос, так как нам нужно углубиться в структуру. (Это не должно требовать фактического соединения, поскольку в качестве внешнего ключа используется testrun uuid.)
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public class CourseSpecifications { /** * Creates a specification used to find courses with the specified testUuid. * * @param testRun * @return */ public static Specification<Course> testRunIs( final TestRun testRun) { return new Specification<Course>() { @Override public Predicate toPredicate(Root<Course> courseRoot, CriteriaQuery<?> query, CriteriaBuilder cb) { Predicate p = null ; if (testRun == null || testRun.getUuid() == null ) { p = cb.isNull(courseRoot.<Course_> get( "testRun" )); } else { p = cb.equal(courseRoot.<Course_> get( "testRun" ).<TestRun_> get( "uuid" ), testRun.getUuid()); } return p; } }; } } |
В некоторых документах предлагается, чтобы я мог использовать get (Course_.testRun) вместо get («testRun»), но eclipse помечал его как нарушение типа в методе get () . Ваш пробег может варьироваться.
Spring Data Repository
Мы должны сообщить Spring Data, что мы используем запросы JPA Criteria. Это делается путем расширения интерфейса JpaSpecificationExecutor .
1
2
3
4
5
6
7
8
|
@Repository public interface CourseRepository extends JpaRepository<Course, Integer>, JpaSpecificationExecutor<Course> { Course findCourseByUuid(String uuid); List findCoursesByTestRunUuid(String uuid); } |
Внедрение FinderService
Теперь мы можем использовать спецификацию JPA в наших реализациях сервиса. Как упомянуто выше, использование спецификации критериев JPA нарушает инкапсуляцию.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
import static com.invariantproperties.sandbox.student.specification.CourseSpecifications.testRunIs; @Service public class CourseFinderServiceImpl implements CourseFinderService { @Resource private CourseRepository courseRepository; /** * @see com.invariantproperties.sandbox.student.business.FinderService# * count() */ @Transactional (readOnly = true ) @Override public long count() { return countByTestRun( null ); } /** * @see com.invariantproperties.sandbox.student.business.FinderService# * countByTestRun(com.invariantproperties.sandbox.student.domain.TestRun) */ @Transactional (readOnly = true ) @Override public long countByTestRun(TestRun testRun) { long count = 0 ; try { count = courseRepository.count(testRunIs(testRun)); } catch (DataAccessException e) { if (!(e instanceof UnitTestException)) { log.info( "internal error retrieving classroom count by " + testRun, e); } throw new PersistenceException( "unable to count classrooms by " + testRun, e, 0 ); } return count; } /** * @see com.invariantproperties.sandbox.student.business.CourseFinderService# * findAllCourses() */ @Transactional (readOnly = true ) @Override public List<Course> findAllCourses() { return findCoursesByTestRun( null ); } /** * @see com.invariantproperties.sandbox.student.business.CourseFinderService# * findCoursesByTestRun(java.lang.String) */ @Transactional (readOnly = true ) @Override public List<Course> findCoursesByTestRun(TestRun testRun) { List<Course> courses = null ; try { courses = courseRepository.findAll(testRunIs(testRun)); } catch (DataAccessException e) { if (!(e instanceof UnitTestException)) { log.info( "error loading list of courses: " + e.getMessage(), e); } throw new PersistenceException( "unable to get list of courses." , e); } return courses; } .... } |
Модульное тестирование
Наши модульные тесты требуют небольшого изменения для использования спецификации.
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
public class CourseFinderServiceImplTest { private final Class<Specification<Course>> sClass = null ; @Test public void testCount() { final long expected = 3 ; final CourseRepository repository = Mockito.mock(CourseRepository. class ); when(repository.count(any(sClass))).thenReturn(expected); final CourseFinderService service = new CourseFinderServiceImpl(repository); final long actual = service.count(); assertEquals(expected, actual); } @Test public void testCountByTestRun() { final long expected = 3 ; final TestRun testRun = new TestRun(); final CourseRepository repository = Mockito.mock(CourseRepository. class ); when(repository.count(any(sClass))).thenReturn(expected); final CourseFinderService service = new CourseFinderServiceImpl(repository); final long actual = service.countByTestRun(testRun); assertEquals(expected, actual); } @Test (expected = PersistenceException. class ) public void testCountError() { final CourseRepository repository = Mockito.mock(CourseRepository. class ); when(repository.count(any(sClass))).thenThrow( new UnitTestException()); final CourseFinderService service = new CourseFinderServiceImpl(repository); service.count(); } @Test public void testFindAllCourses() { final List<Course> expected = Collections.emptyList(); final CourseRepository repository = Mockito.mock(CourseRepository. class ); when(repository.findAll(any(sClass))).thenReturn(expected); final CourseFinderService service = new CourseFinderServiceImpl(repository); final List<Course> actual = service.findAllCourses(); assertEquals(expected, actual); } @Test (expected = PersistenceException. class ) public void testFindAllCoursesError() { final CourseRepository repository = Mockito.mock(CourseRepository. class ); final Class<Specification<Course>> sClass = null ; when(repository.findAll(any(sClass))).thenThrow( new UnitTestException()); final CourseFinderService service = new CourseFinderServiceImpl(repository); service.findAllCourses(); } @Test public void testFindCourseByTestUuid() { final TestRun testRun = new TestRun(); final Course course = new Course(); final List<Course> expected = Collections.singletonList(course); final CourseRepository repository = Mockito.mock(CourseRepository. class ); when(repository.findAll(any(sClass))).thenReturn(expected); final CourseFinderService service = new CourseFinderServiceImpl(repository); final List actual = service.findCoursesByTestRun(testRun); assertEquals(expected, actual); } @Test (expected = PersistenceException. class ) public void testFindCourseByTestUuidError() { final TestRun testRun = new TestRun(); final CourseRepository repository = Mockito.mock(CourseRepository. class ); when(repository.findAll(any(sClass))).thenThrow( new UnitTestException()); final CourseFinderService service = new CourseFinderServiceImpl(repository); service.findCoursesByTestRun(testRun); } @Test public void testFindCoursesByTestUuid() { final TestRun testRun = new TestRun(); final Course course = new Course(); final List<Course> expected = Collections.singletonList(course); final CourseRepository repository = Mockito.mock(CourseRepository. class ); when(repository.findAll(any(sClass))).thenReturn(expected); final CourseFinderService service = new CourseFinderServiceImpl(repository); final List<Course> actual = service.findCoursesByTestRun(testRun); assertEquals(expected, actual); } @Test (expected = PersistenceException. class ) public void testFindCoursesByTestUuidError() { final TestRun testRun = new TestRun(); final CourseRepository repository = Mockito.mock(CourseRepository. class ); when(repository.findAll(any(sClass))).thenThrow( new UnitTestException()); final CourseFinderService service = new CourseFinderServiceImpl(repository); service.findCoursesByTestRun(testRun); } .... } |
Я мог бы устранить много дублирующегося кода, используя метод @Begin, но решил не поддерживать параллельное тестирование.
Интеграционное тестирование
Наконец мы подошли к нашим интеграционным тестам. Мы знаем, что сделали что-то правильно, потому что единственная строка для проверки дополнительной функциональности — подсчет количества курсов в базе данных.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
@RunWith (SpringJUnit4ClassRunner. class ) @ContextConfiguration (classes = { BusinessApplicationContext. class , TestBusinessApplicationContext. class , TestPersistenceJpaConfig. class }) @Transactional @TransactionConfiguration (defaultRollback = true ) public class CourseServiceIntegrationTest { @Resource private CourseFinderService fdao; @Resource private CourseManagerService mdao; @Resource private TestRunService testService; @Test public void testCourseLifecycle() throws Exception { final TestRun testRun = testService.createTestRun(); final String name = "Calculus 101 : " + testRun.getUuid(); final Course expected = new Course(); expected.setName(name); assertNull(expected.getId()); // create course Course actual = mdao.createCourseForTesting(name, testRun); expected.setId(actual.getId()); expected.setUuid(actual.getUuid()); expected.setCreationDate(actual.getCreationDate()); assertThat(expected, equalTo(actual)); assertNotNull(actual.getUuid()); assertNotNull(actual.getCreationDate()); // get course by id actual = fdao.findCourseById(expected.getId()); assertThat(expected, equalTo(actual)); // get course by uuid actual = fdao.findCourseByUuid(expected.getUuid()); assertThat(expected, equalTo(actual)); // get all courses final List<Course> courses = fdao.findCoursesByTestRun(testRun); assertTrue(courses.contains(actual)); // count courses final long count = fdao.countByTestRun(testRun); assertTrue(count > 0 ); // update course expected.setName( "Calculus 102 : " + testRun.getUuid()); actual = mdao.updateCourse(actual, expected.getName()); assertThat(expected, equalTo(actual)); // delete Course mdao.deleteCourse(expected.getUuid(), 0 ); try { fdao.findCourseByUuid(expected.getUuid()); fail( "exception expected" ); } catch (ObjectNotFoundException e) { // expected } testService.deleteTestRun(testRun.getUuid()); } } |
Исходный код
- Исходный код доступен по адресу http://code.google.com/p/invariant-properties-blog/source/browse/student .