Persistence
Now that we have a domain model, we would like to use some persistence with it. In Domain-Driven Design, persistence is done with Repositories which work on whole aggregates.
We need some data first!
To be able to test this, we need some sample data. A class implementing LifecycleListener
will provide the opportunity to insert data at application startup.
In the package infrastructure
, create a SampleDataGenerator
class:
package org.generated.project.infrastructure;
import javax.inject.Inject;
import org.generated.project.domain.model.person.Person;
import org.generated.project.domain.model.person.PersonId;
import org.seedstack.business.domain.Repository;
import org.seedstack.business.util.inmemory.InMemory;
import org.seedstack.seed.LifecycleListener;
public class SampleDataGenerator implements LifecycleListener {
@Inject
@InMemory
private Repository<Person, PersonId> personRepository;
@Override
public void started() {
personRepository.addOrUpdate(create("bill.evans@some.org", "Bill", "EVANS"));
personRepository.addOrUpdate(create("ella.fitzgerald@some.org", "Ella", "FITZGERALD"));
personRepository.addOrUpdate(create("miles.davis@some.org", "Miles", "DAVIS"));
}
private Person create(String email, String firstName, String lastName) {
Person person = new Person(new PersonId(email));
person.changeName(firstName, lastName);
return person;
}
}
The default repository
As we don’t want to write unnecessary code, we will use a default repository implementation of SeedStack.
To do that, just inject the parameterized Repository
interface in your
HelloResource
. We will combine it with the service we defined before:
package org.generated.project.interfaces.rest;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.generated.project.domain.model.person.Person;
import org.generated.project.domain.model.person.PersonId;
import org.generated.project.domain.services.GreeterService;
import org.seedstack.business.domain.Repository;
import org.seedstack.business.util.inmemory.InMemory;
@Path("hello")
public class HelloResource {
@Inject
@InMemory
private Repository<Person, PersonId> personRepository;
@Inject
private GreeterService greeterService;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return personRepository.get(new PersonId("bill.evans@some.org"))
.map(greeterService::greet)
.orElseThrow(NotFoundException::new);
}
}
Multiple repository implementations can co-exist, so we need to choose one by qualifying the injection point. An in-memory
implementation is enough for now, so we use the @InMemory
qualifier.
Custom query
As business needs evolve, you will surely need to implement custom queries to retrieve aggregates.
With SeedStack, you can build custom queries on the object model, regardless of the persistence implementation. This is done in a custom repository interface, with a specification.
In the domain.model.person
package, create the PersonRepository
interface, extending Repository
:
package org.generated.project.domain.model.person;
import java.util.stream.Stream;
import org.seedstack.business.domain.Repository;
public interface PersonRepository extends Repository<Person, PersonId> {
default Stream<Person> findByName(String name) {
return get(getSpecificationBuilder().of(Person.class)
.property("firstName").matching(name).ignoringCase()
.or()
.property("lastName").matching(name).ignoringCase()
.build()
);
}
}
We can now inject this custom interface in the HelloResource
class instead of the default repository:
package org.generated.project.interfaces.rest;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.generated.project.domain.model.person.PersonRepository;
import org.generated.project.domain.services.GreeterService;
import org.seedstack.business.util.inmemory.InMemory;
@Path("hello")
public class HelloResource {
@Inject
@InMemory
private PersonRepository personRepository;
@Inject
private GreeterService greeterService;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return personRepository.findByName("ella")
.findFirst()
.map(greeterService::greet)
.orElseThrow(NotFoundException::new);
}
}
For now, hot-reloading (with seedstack:watch
) doesn’t play well with the dynamic implementation generated for
a repository without explicit implementation.
If you wish to make changes to the PersonRepository
, you’ll have to restart the watch process manually.
Switch to JPA persistence
To demonstrate that, with the code above, we have true independence from database technology, let’s switch to a JPA based persistence layer.
As we will add new dependencies and some configuration, the application must be stopped.
Dependencies
Add the SeedStack JPA add-on:
<dependency>
<groupId>org.seedstack.addons.jpa</groupId>
<artifactId>jpa</artifactId>
</dependency>
Show version
dependencies {
compile("org.seedstack.addons.jpa:jpa:4.2.0")
}
Then add an implementation of the JPA standard like Hibernate:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.3.7.Final</version>
</dependency>
dependencies {
compile("org.hibernate:hibernate-entitymanager:5.3.7.Final")
}
Hibernate requires the following dependency if you’re using Java 9 or more recent:
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
dependencies {
compile("javax.xml.bind:jaxb-api:2.3.0")
}
Then add an in-memory capable database like HyperSQL:
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.4.1</version>
</dependency>
dependencies {
compile("org.hsqldb:hsqldb:2.4.1")
}
To connect to an external database, you would add its JDBC driver dependency instead.
Configure a database
JPA is structured around the concept of «persistence units». We have to configure at least one unit to use JPA and such unit needs
a JDBC datasource to work with. Add the following configuration in the application.yaml
file:
jdbc:
datasources:
myDatasource:
url: jdbc:hsqldb:mem:mydb
The configuration above will declare a JDBC datasource named myDatasource
pointing to an auto-created, in-memory,
H2 database. Now declare a JPA unit using this datasource:
jpa:
units:
myUnit:
datasource: myDatasource
properties:
hibernate.dialect: org.hibernate.dialect.HSQLDialect
hibernate.hbm2ddl.auto: update
The configuration above will declare a JPA unit named myUnit
, referencing our datasource.
The properties are here to configure Hibernate, our JPA provider and are specific to it. Their role here is to tell Hibernate what SQL dialect should be used and that we need the tables to be created or updated on startup.
Add the JPA annotations
JPA entities must be mapped to a relational model. While this can be done through XML mapping files, a simpler way is to annotate the classes.
The PersonId class becomes:
package org.generated.project.domain.model.person;
import javax.persistence.Embeddable;
import org.seedstack.business.domain.BaseValueObject;
@Embeddable
public class PersonId extends BaseValueObject {
private String email;
private PersonId() {
// needed for Hibernate
}
public PersonId(String email) {
this.email = email;
}
// ...
}
The Person class becomes:
package org.generated.project.domain.model.person;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import org.seedstack.business.domain.BaseAggregateRoot;
@Entity
public class Person extends BaseAggregateRoot<PersonId> {
@EmbeddedId
private PersonId id;
private String firstName;
private String lastName;
private Person() {
// needed for Hibernate
}
public Person(PersonId id) {
this.id = id;
}
// ...
}
Note that we also added a private no-arg constructor to allow Hibernate to instantiate the classes.
Change the qualifiers
Now just replace the @InMemory
injection qualifier with
the @Jpa
qualifier common from the JPA add-on. This will tell SeedStack to choose
a JPA-based implementation (also provided by the add-on) for injection instead of the in-memory one.
Let’s start with the SampleDataGenerator
qualifier:
public class SampleDataGenerator implements LifecycleListener {
@Inject
@InMemory
private Repository<Person, PersonId> personRepository;
// ...
}
Becomes:
public class SampleDataGenerator implements LifecycleListener {
@Inject
@Jpa
private Repository<Person, PersonId> personRepository;
// ...
}
Then the HelloResource class:
@Path("hello")
public class HelloResource {
@Inject
@InMemory
private PersonRepository personRepository;
// ...
}
Becomes:
@Path("hello")
public class HelloResource {
@Inject
@Jpa
private PersonRepository personRepository;
// ...
}
Declare a transaction
JPA database work should be done in a transaction. A good place to start and end a transaction would be on an application
service that orchestrates all the needed operations for a particular use-case. We don’t have such a service in our little
tutorial, so we will put the transaction on the hello()
method of our REST resource.
Two annotations are needed here:
- The
@Transactional
annotation to declare the transaction boundaries, - And the
@JpaUnit
annotation to declare on which resource the transaction should be done.
In the HelloResource
class:
@Path("hello")
public class HelloResource {
// ...
@GET
@Produces(MediaType.TEXT_PLAIN)
@Transactional
@JpaUnit("myUnit")
public String hello() {
// ...
}
}
And in the SampleDataGenerator
class:
public class SampleDataGenerator implements LifecycleListener {
/// ...
@Override
@Transactional
@JpaUnit("myUnit")
public void started() {
// ...
}
// ...
}
Run the application again
Startup the application again by issuing the following command:
mvn seedstack:watch
You can now see that the /hello
REST endpoint shows the same behavior as before but backed by JPA persistence. You
can see the JPA subsystem being initialized in the application startup logs.
What happened ?
Business-framework compatible persistence add-ons (like JPA or MongoDB) each provide:
- An implementation for all standard operations for repositories.
- The ability to translate a domain specification into the corresponding technology-specific query.
Both are used here, behind the scenes. The specification used in the findByName()
method gets translated into a JPA
criteria query, turned then by Hibernate into the proper SQL query. The results are available as a Stream
of domain objects, loaded dynamically from the database as they are consumed.
Now what ?
What we learned
In this page you have learned:
- How to execute code at application startup, here to create sample data,
- How to use the default repository implementation without writing any code,
- How to write a technology-agnostic custom query with a specification,
- How to configure and use an in-memory database with JPA and hibernate.
If you want to go further on the topic of persistence, see what you can read about repositories and specifications.
You can also explore SeedStack persistence add-ons documentation: JDBC, JPA, MongoDB, etc…
Troubleshooting
If you can’t get this to work, check the troubleshooting page.
Next step
If you want to learn more, continue on the tutorial on REST API.