Статьи

Студент проекта: постоянство с весенними данными

Это часть проекта Студент . Другие сообщения — Клиент Webservice с Джерси , Сервер Webservice с Джерси и Бизнес-уровень .

Последний слой веб-приложения RESTful — это слой постоянства.

Есть две философии для постоянства слоев. Один лагерь рассматривает базу данных как простое хранилище и хочет сохранить этот слой чрезвычайно тонким. Другой лагерь знает, что часто гораздо быстрее выполнять задачи в базе данных, чем обращаться к базе данных за данными, выполнять необходимую работу в java и, возможно, во второй раз попадать в базу данных с результатами. Этот лагерь часто хочет довольно толстый слой постоянства.

Во втором лагере также есть группа ссыльных, которая хочет широко использовать хранимые процедуры. Это делает базу данных намного более мощной, но за счет некоторой дополнительной сложности на уровне постоянства. Он имеет главный недостаток в том, что хранимые процедуры зависят от базы данных.

Вторая группа ссыльных просто использует хранимые процедуры для безопасности. Я обсуждал это ранее, например, идею о том, что вы должны вызывать хранимую процедуру с именем пользователя и паролем и получать ответ «принять» или «отказаться» вместо того, чтобы извлекать хешированный пароль и делать сравнение в приложении. Первый подход позволяет использовать привилегии базы данных GRANT и REVOKE для хранения хешированных паролей в недоступной таблице, даже если злоумышленник может выполнить произвольное внедрение SQL как пользователь приложения.

(Замечание: вы часто можете писать свои хранимые процедуры на Java! Oracle поддерживает их, PostgreSQL поддерживает их (через расширение PL / Java), H2 поддерживает их (через путь к классам). Я не знаю, поддерживает ли MySQL это. Этот подход имеет определенные затраты, но это может быть лучшим решением для многих проблем.)

В любом случае, этот проект на данный момент поддерживает только операции CRUD, и это классический пример использования тонкого слоя персистентности. Однако легко добавить «толстые» методы — просто создайте с ними класс CourseRepositoryImpl. (Этот класс НЕ должен реализовывать интерфейс CourseRepository!)

Проектные решения

Spring Data — мы используем Spring Data, потому что он автоматически генерирует классы персистентного слоя.

Ограничения

Нумерация страниц — нет попыток поддержать нумерацию страниц. Это не большая проблема, так как Spring Data уже поддерживает это — нам нужно только написать клей.

конфигурация

Базовая конфигурация требует только аннотации @EnableJpaRepositories .

Интеграционный тест с использованием чистой встроенной базы данных в памяти требует немного больше работы. Непосредственное использование встроенной базы данных Spring не работает, даже если вы используете URL-адрес H2, который говорит ему оставить сервер базы данных включенным. База данных правильно инициализируется, но затем закрывается до того, как тесты действительно запустятся. Результатом являются сбои, так как схема базы данных отсутствует.

Было бы тривиально использовать базу данных в памяти, подкрепленную файлом, но это могло бы вызвать проблемы с одновременными запусками, случайным извлечением старых тестовых данных и т. Д. Очевидное решение заключается в использовании случайного временного резервного файла, но этот подход создает свои собственные проблемы. ,

Подход, приведенный ниже, заключается в кэшировании встроенной базы данных в классе конфигурации и ее уничтожении только при завершении работы приложения. Это вводит некоторое неочевидное поведение, которое заставляет нас явно создавать несколько дополнительных bean-компонентов.

(IIRC, если вы создаете встроенную базу данных в классе конфигурации и компоненты транзакций в файле конфигурации, то Spring создает фиктивный источник данных в файле конфигурации, и инициализация не удалась. Эта проблема исчезла, когда я начал создавать компоненты транзакций в том же место в качестве источника данных.)

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
@Configuration
@EnableJpaRepositories(basePackages = { "com.invariantproperties.sandbox.student.repository" })
@EnableTransactionManagement
@PropertySource("classpath:test-application.properties")
@ImportResource("classpath:applicationContext-dao.xml")
public class TestPersistenceJpaConfig implements DisposableBean {
    private static final Logger log = LoggerFactory.getLogger(TestPersistenceJpaConfig.class);
 
    private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect";
    private static final String PROPERTY_NAME_HIBERNATE_FORMAT_SQL = "hibernate.format_sql";
    private static final String PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY = "hibernate.ejb.naming_strategy";
    private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql";
    private static final String PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto";
    private static final String PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN = "entitymanager.packages.to.scan";
    // private static final String PROPERTY_NAME_PERSISTENCE_UNIT_NAME =
    // "persistence.unit.name";
 
    @Resource
    private Environment environment;
 
    private EmbeddedDatabase db = null;
 
    @Bean
    public DataSource dataSource() {
        final EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        db = builder.setType(EmbeddedDatabaseType.H2).build(); // .script("foo.sql")
        return db;
    }
 
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws ClassNotFoundException {
        LocalContainerEntityManagerFactoryBean bean = new LocalContainerEntityManagerFactoryBean();
 
        bean.setDataSource(dataSource());
        bean.setPackagesToScan(environment.getRequiredProperty(PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN));
        bean.setPersistenceProviderClass(HibernatePersistence.class);
        // bean.setPersistenceUnitName(environment
        // .getRequiredProperty(PROPERTY_NAME_PERSISTENCE_UNIT_NAME));
 
        HibernateJpaVendorAdapter va = new HibernateJpaVendorAdapter();
        bean.setJpaVendorAdapter(va);
 
        Properties jpaProperties = new Properties();
        jpaProperties.put(PROPERTY_NAME_HIBERNATE_DIALECT,
environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT));
        jpaProperties.put(PROPERTY_NAME_HIBERNATE_FORMAT_SQL,
                environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_FORMAT_SQL));
        jpaProperties.put(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY,
                environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_NAMING_STRATEGY));
        jpaProperties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL,
                environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL));
        jpaProperties.put(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO,
                environment.getRequiredProperty(PROPERTY_NAME_HIBERNATE_HBM2DDL_AUTO));
 
        bean.setJpaProperties(jpaProperties);
 
        return bean;
    }
 
    @Bean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager tm = new JpaTransactionManager();
 
        try {
            tm.setEntityManagerFactory(this.entityManagerFactory().getObject());
        } catch (ClassNotFoundException e) {
            // TODO: log.
        }
 
        return tm;
    }
 
    @Bean
    public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
        return new PersistenceExceptionTranslationPostProcessor();
    }
 
    @Override
    public void destroy() {
        if (db != null) {
            db.shutdown();
        }
    }
}

Файл applicationContext.xml пуст. Файл свойств:

01
02
03
04
05
06
07
08
09
10
11
# hibernate configuration
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy
hibernate.show_sql=false
hibernate.format_sql=true
hibernate.hbm2ddl.auto=create
 
# jpa configuration
entitymanager.packages.to.scan=com.invariantproperties.sandbox.student.domain
persistence.unit.dataSource=java:comp/env/jdbc/ssDS
persistence.unit.name=ssPU

Модульное тестирование

Там нет модульных тестов, так как весь код генерируется автоматически. Это изменится по мере добавления пользовательских методов.

Интеграционное тестирование

Теперь мы можем написать интеграционные тесты для бизнес-уровня:

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { BusinessApplicationContext.class, TestBusinessApplicationContext.class,
        TestPersistenceJpaConfig.class })
@Transactional
@TransactionConfiguration(defaultRollback = true)
public class CourseServiceIntegrationTest {
 
    @Resource
    private CourseService dao;
 
    @Test
    public void testCourseLifecycle() throws Exception {
        final String name = "Calculus 101";
 
        final Course expected = new Course();
        expected.setName(name);
 
        assertNull(expected.getId());
 
        // create course
        Course actual = dao.createCourse(name);
        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 = dao.findCourseById(expected.getId());
        assertThat(expected, equalTo(actual));
 
        // get course by uuid
        actual = dao.findCourseByUuid(expected.getUuid());
        assertThat(expected, equalTo(actual));
 
        // update course
        expected.setName("Calculus 102");
        actual = dao.updateCourse(actual, expected.getName());
        assertThat(expected, equalTo(actual));
 
        // delete Course
        dao.deleteCourse(expected.getUuid());
        try {
            dao.findCourseByUuid(expected.getUuid());
            fail("exception expected");
        } catch (ObjectNotFoundException e) {
            // expected
        }
    }
 
    /**
     * @test findCourseById() with unknown course.
     */
    @Test(expected = ObjectNotFoundException.class)
    public void testfindCourseByIdWhenCourseIsNotKnown() {
        final Integer id = 1;
        dao.findCourseById(id);
    }
 
    /**
     * @test findCourseByUuid() with unknown Course.
     */
    @Test(expected = ObjectNotFoundException.class)
    public void testfindCourseByUuidWhenCourseIsNotKnown() {
        final String uuid = "missing";
        dao.findCourseByUuid(uuid);
    }
 
    /**
     * Test updateCourse() with unknown course.
     *
     * @throws ObjectNotFoundException
     */
    @Test(expected = ObjectNotFoundException.class)
    public void testUpdateCourseWhenCourseIsNotFound() {
        final Course course = new Course();
        course.setUuid("missing");
        dao.updateCourse(course, "Calculus 102");
    }
 
    /**
     * Test deleteCourse() with unknown course.
     *
     * @throws ObjectNotFoundException
     */
    @Test(expected = ObjectNotFoundException.class)
    public void testDeleteCourseWhenCourseIsNotFound() {
        dao.deleteCourse("missing");
    }
}

Исходный код