Enterprise Java

Project Student: Persistence With Spring Data

This is part of Project Student. Other posts are Webservice Client With Jersey, Webservice Server with Jersey and Business Layer.

The final layer of the RESTful webapp onion is the persistence layer.

There are two philosophies for persistence layers. One camp sees the database as a simple store and wants to keep this layer extremely thin. The other camp knows it’s often much faster to perform tasks in the database than to hit the database for the data, do the necessary work in java, and possibly hit the database a second time with the results. This camp often wants a fairly thick persistence layer.

The second camp also has a group of exiles that wants to use stored procedures extensively. This makes the database much more powerful but at the cost of a bit of additional complexity in the persistence layer. It has a major drawback in that stored procedures are database-specific.

A second group of exiles just uses stored procedures for security. I’ve discussed this earlier, e.g., the idea that you should call a stored procedure with username and password and get an “accept” or “deny” response instead of retrieving the hashed password and doing the comparison in the application. The first approach allows you to use the database’s GRANT and REVOKE privileges to store hashed passwords in a table that’s inaccessible even if an attacker can perform arbitrary SQL injection as the application user.

(Sidenote: you can often write your stored procedures in Java! Oracle supports it, PostgreSQL supports it (via PL/Java extension), H2 supports it (via the classpath). I don’t know if MySQL supports it. This approach has definite costs but it may be the best solution for many problems.)

Anyway this project only supports CRUD operations at the moment and they’re a classic example of using a thin persistence layer. It’s easy to add ‘thick’ methods though – simply create a CourseRepositoryImpl class with them. (This class should NOT implement the CourseRepository interface!)

Design Decisions

Spring Data – we’re using Spring Data because it autogenerates the persistence layer classes.

Limitations

Pagination – there is no attempt to support pagination. This isn’t a big issue since Spring Data already supports it – we only need to write the glue.

Configuration

The basic configuration only requires the @EnableJpaRepositories annotation.

The integration test using a pure in-memory embedded database requires a bit more work. Using the Spring embedded database directly doesn’t work even if you use the H2 url that tells it to leave the database server up. The database is properly initialized but then closed before the tests actually run. The result is failures since the database schema is missing.

It would be trivial to use an in-memory database backed by a file but that could cause problems with concurrent runs, accidently pulling in old test data, etc. The obvious solution is using a random temporary backing file but that approach introduces its own problems.

The approach below is to cache the embedded database in the configuration class and only destroy it as the app shuts down. This introduces some non-obvious behavior that forces us to explicitly create a few additional beans as well.

(IIRC if you create the embedded database in a configuration class and the transaction beans in a configuration file then spring was creating a phantom datasource in the configuration file and initialization failed. This problem went away when I started to create the transaction beans in the same place as the datasource.)

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

The applicationContext.xml file is empty. The properties file is:

# 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

Unit Testing

There are no unit tests since all of the code is autogenerated. This will change as custom methods are added.

Integration Testing

We can now write the integration tests for the business layer:

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

Source Code

 

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button