Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support
@UnitOfWork
in sub-resources
Currently, Dropwizard doesn't open transactions in sub-resources as reported in #1806. The problem is that Dropwizard scans resource methods for `@UnitOfWork` during resource initilization and sub-resource methods are not resolved at that time and can't be registered by Dropwizard. The fix is to defer the lookup of the `@UnitOfWork` annotation on a method until it's invoked and than cache it. One downside of this approache is that `ConcurrentMap` doesn't support null values, so we have to use `Optional` from `UnitOfWork` which can add an additional overhead, but it should be negligible.
- Loading branch information
Showing
4 changed files
with
276 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
243 changes: 243 additions & 0 deletions
243
dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/SubResourcesTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
package io.dropwizard.hibernate; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import io.dropwizard.Application; | ||
import io.dropwizard.Configuration; | ||
import io.dropwizard.db.DataSourceFactory; | ||
import io.dropwizard.db.PooledDataSourceFactory; | ||
import io.dropwizard.setup.Bootstrap; | ||
import io.dropwizard.setup.Environment; | ||
import io.dropwizard.testing.ResourceHelpers; | ||
import io.dropwizard.testing.junit.DropwizardAppRule; | ||
import org.hibernate.Session; | ||
import org.hibernate.SessionFactory; | ||
import org.hibernate.Transaction; | ||
import org.junit.ClassRule; | ||
import org.junit.Test; | ||
|
||
import javax.ws.rs.*; | ||
import javax.ws.rs.client.Entity; | ||
import javax.ws.rs.core.MediaType; | ||
import javax.ws.rs.core.Response; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
public class SubResourcesTest { | ||
|
||
public static class TestConfiguration extends Configuration { | ||
|
||
DataSourceFactory dataSource = new DataSourceFactory(); | ||
|
||
TestConfiguration(@JsonProperty("dataSource") DataSourceFactory dataSource) { | ||
this.dataSource = dataSource; | ||
} | ||
} | ||
|
||
public static class TestApplication extends Application<TestConfiguration> { | ||
final HibernateBundle<TestConfiguration> hibernate = new HibernateBundle<TestConfiguration>(Person.class, Dog.class) { | ||
@Override | ||
public PooledDataSourceFactory getDataSourceFactory(TestConfiguration configuration) { | ||
return configuration.dataSource; | ||
} | ||
}; | ||
|
||
@Override | ||
public void initialize(Bootstrap<TestConfiguration> bootstrap) { | ||
bootstrap.addBundle(hibernate); | ||
} | ||
|
||
@Override | ||
public void run(TestConfiguration configuration, Environment environment) throws Exception { | ||
final SessionFactory sessionFactory = hibernate.getSessionFactory(); | ||
initDatabase(sessionFactory); | ||
|
||
environment.jersey().register(new UnitOfWorkApplicationListener("hr-db", sessionFactory)); | ||
environment.jersey().register(new PersonResource(new PersonDAO(sessionFactory), new DogDAO(sessionFactory))); | ||
} | ||
|
||
private void initDatabase(SessionFactory sessionFactory) { | ||
try (Session session = sessionFactory.openSession()) { | ||
Transaction transaction = session.beginTransaction(); | ||
session.createNativeQuery("CREATE TABLE people (name varchar(100) primary key, email varchar(16), birthday timestamp)") | ||
.executeUpdate(); | ||
session.createNativeQuery("INSERT INTO people VALUES ('Greg', 'greg@yahooo.com', '1989-02-13')") | ||
.executeUpdate(); | ||
session.createNativeQuery("CREATE TABLE dogs (name varchar(100) primary key, owner varchar(100) REFERENCES people(name))") | ||
.executeUpdate(); | ||
session.createNativeQuery("INSERT INTO dogs VALUES ('Bello', 'Greg')") | ||
.executeUpdate(); | ||
transaction.commit(); | ||
} | ||
} | ||
} | ||
|
||
@Produces(MediaType.APPLICATION_JSON) | ||
@Path("/people") | ||
public static class PersonResource { | ||
private final PersonDAO personDao; | ||
private final DogResource dogResource; | ||
|
||
PersonResource(PersonDAO dao, DogDAO dogDao) { | ||
this.personDao = dao; | ||
this.dogResource = new DogResource(dogDao, personDao); | ||
} | ||
|
||
@GET | ||
@Path("{name}") | ||
@UnitOfWork(readOnly = true) | ||
public Optional<Person> find(@PathParam("name") String name) { | ||
return personDao.findByName(name); | ||
} | ||
|
||
@POST | ||
@UnitOfWork | ||
public Person save(Person person) { | ||
return personDao.persist(person); | ||
} | ||
|
||
@Path("/{ownerName}/dogs") | ||
public DogResource dogResource() { | ||
return dogResource; | ||
} | ||
} | ||
|
||
@Produces(MediaType.APPLICATION_JSON) | ||
public static class DogResource { | ||
|
||
private final DogDAO dogDAO; | ||
private final PersonDAO personDAO; | ||
|
||
DogResource(DogDAO dogDAO, PersonDAO personDAO) { | ||
this.dogDAO = dogDAO; | ||
this.personDAO = personDAO; | ||
} | ||
|
||
@GET | ||
// Intentionally no `@UnitOfWork` | ||
public List<Dog> findAll(@PathParam("ownerName") String ownerName) { | ||
return dogDAO.findByOwner(ownerName); | ||
} | ||
|
||
@GET | ||
@Path("{dogName}") | ||
@UnitOfWork(readOnly = true) | ||
public Optional<Dog> find(@PathParam("ownerName") String ownerName, | ||
@PathParam("dogName") String dogName) { | ||
return dogDAO.findByOwnerAndName(ownerName, dogName); | ||
} | ||
|
||
@POST | ||
@UnitOfWork | ||
public Dog create(@PathParam("ownerName") String ownerName, Dog dog) { | ||
Optional<Person> person = personDAO.findByName(ownerName); | ||
if (!person.isPresent()) { | ||
throw new WebApplicationException(404); | ||
} | ||
dog.setOwner(person.get()); | ||
return dogDAO.persist(dog); | ||
} | ||
} | ||
|
||
public static class PersonDAO extends AbstractDAO<Person> { | ||
PersonDAO(SessionFactory sessionFactory) { | ||
super(sessionFactory); | ||
} | ||
|
||
Optional<Person> findByName(String name) { | ||
return Optional.ofNullable(get(name)); | ||
} | ||
} | ||
|
||
public static class DogDAO extends AbstractDAO<Dog> { | ||
DogDAO(SessionFactory sessionFactory) { | ||
super(sessionFactory); | ||
} | ||
|
||
Optional<Dog> findByOwnerAndName(String ownerName, String dogName) { | ||
return query("SELECT d FROM Dog d WHERE d.owner.name=:owner AND d.name=:name") | ||
.setParameter("owner", ownerName) | ||
.setParameter("name", dogName) | ||
.uniqueResultOptional(); | ||
} | ||
|
||
List<Dog> findByOwner(String ownerName) { | ||
return query("SELECT d FROM Dog d WHERE d.owner.name=:owner") | ||
.setParameter("owner", ownerName) | ||
.list(); | ||
} | ||
} | ||
|
||
@ClassRule | ||
public static DropwizardAppRule<TestConfiguration> APP_RULE = new DropwizardAppRule<>(TestApplication.class, | ||
ResourceHelpers.resourceFilePath("hibernate-sub-resource-test.yaml")); | ||
|
||
private static String baseUri() { | ||
return "http://localhost:" + APP_RULE.getLocalPort(); | ||
} | ||
|
||
@Test | ||
public void canReadFromTopResource() throws Exception { | ||
final Person person = APP_RULE.client() | ||
.target(baseUri() + "/people/Greg") | ||
.request() | ||
.get(Person.class); | ||
|
||
assertThat(person.getName()).isEqualTo("Greg"); | ||
} | ||
|
||
@Test | ||
public void canWriteTopResource() throws Exception { | ||
final Person person = APP_RULE.client() | ||
.target(baseUri() + "/people") | ||
.request() | ||
.post(Entity.entity("{\"name\": \"Jason\", \"email\": \"jason@gmail.com\", \"birthday\":637317407000}", | ||
MediaType.APPLICATION_JSON_TYPE), Person.class); | ||
|
||
assertThat(person.getName()).isEqualTo("Jason"); | ||
} | ||
|
||
@Test | ||
public void canReadFromSubResources() throws Exception { | ||
final Dog dog = APP_RULE.client() | ||
.target(baseUri() + "/people/Greg/dogs/Bello") | ||
.request() | ||
.get(Dog.class); | ||
|
||
assertThat(dog.getName()).isEqualTo("Bello"); | ||
assertThat(dog.getOwner()).isNotNull(); | ||
assertThat(dog.getOwner().getName()).isEqualTo("Greg"); | ||
} | ||
|
||
@Test | ||
public void canWriteSubResource() throws Exception { | ||
final Dog dog = APP_RULE.client() | ||
.target(baseUri() + "/people/Greg/dogs") | ||
.request() | ||
.post(Entity.entity("{\"name\": \"Bandit\"}", MediaType.APPLICATION_JSON_TYPE), Dog.class); | ||
|
||
assertThat(dog.getName()).isEqualTo("Bandit"); | ||
assertThat(dog.getOwner()).isNotNull(); | ||
assertThat(dog.getOwner().getName()).isEqualTo("Greg"); | ||
} | ||
|
||
@Test | ||
public void errorsAreHandled() throws Exception { | ||
Response response = APP_RULE.client() | ||
.target(baseUri() + "/people/Jim/dogs") | ||
.request() | ||
.post(Entity.entity("{\"name\": \"Bullet\"}", MediaType.APPLICATION_JSON_TYPE)); | ||
assertThat(response.getStatus()).isEqualTo(404); | ||
} | ||
|
||
@Test | ||
public void noSessionErrorIsRaised() throws Exception { | ||
Response response = APP_RULE.client() | ||
.target(baseUri() + "/people/Greg/dogs") | ||
.request() | ||
.get(); | ||
assertThat(response.getStatus()).isEqualTo(500); | ||
} | ||
|
||
} |
10 changes: 10 additions & 0 deletions
10
dropwizard-hibernate/src/test/resources/hibernate-sub-resource-test.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
server: | ||
applicationConnectors: | ||
- type: http | ||
port: 0 | ||
adminConnectors: | ||
- type: http | ||
port: 0 | ||
dataSource: | ||
url: 'jdbc:h2:mem:sub-resources-test' | ||
driverClass: "org.h2.Driver" |