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
- The source code is available at http://code.google.com/p/invariant-properties-blog/source/browse/student/student-business and http://code.google.com/p/invariant-properties-blog/source/browse/student/student-persistence.