Imagine the following: tech lead asks you to come up ASAP with a project boilerplate including:
- Latest Spring boot version
- Cutting-edge graph database support
- reactive CRUD API
- Database seeder for your dev environment
Whatcha gonna do?
1. Spring into action
With the power of our favourite http client we swiftly fetch a ready-to-go project structure from the beautiful Spring starter API just like that:
curl https://start.spring.io/starter.tgz \
-d dependencies=webflux,data-neo4j,lombok \
-d type=gradle-project \
-d JavaVersion=21 \
-d bootVersion=3.3.4 \
-d baseDir=matrix \
-d groupId=org.thematrix \
-d artifactId=library \
-d name=Library | tar -xzvf -
Pretty neat, huh? Let's enter the matrix:
cd matrix
2. "I know why you're here, Neo" π
We'll use a neo4j service with data volume binding and expose the database server default ports to our host network.
services:
neo4j:
image: "neo4j:5.23"
environment:
NEO4J_AUTH: $DB_USER/$DB_PWD # `none` to disable
volumes:
- $HOME/.neo4j/data:/data
- $HOME/.neo4j/conf:/var/lib/neo4j/conf
ports:
- 7474:7474 # ui
- 7687:7687 # bolt
Bonus: Having the database configuration files (conf/neo4j.conf
) at hand is mighty useful; it lets us e.g. optimize for read/write resp., configure transaction behaviour, disable unwanted background services etc.
// Incoming defaults
server.memory.pagecache.size=512M
server.default_listen_address=0.0.0.0
server.directories.logs=/logs
// other defaults
db.transaction.concurrent.maximum=1000
db.transaction.timeout=0
// add this
dbms.usage_report.enabled=false
Bridging the gap
Nice, all up and running! Now we'll need to tell our Spring boot application in src/main/resources/application.properties
how to connect to the database system:
spring.neo4j.uri=bolt://${DB_URL}:7687
spring.neo4j.authentication.username=${DB_USER}
spring.neo4j.authentication.password=${DB_PWD}
Make sure we have auth data to work with.
echo "DB_URL=localhost\nDB_USER=neo4j\nDB_PWD=password" > .env
Spilling the Beans
Before we get to play with our DTOs, we'll need to define a couple of database config beans (injected singleton dependencies) to the Spring IoC Container. We'll have a separate AppConfig
class to hold 'em:
@org.springframework.context.annotation.Configuration
@EnableReactiveNeo4jRepositories(basePackages = "repos")
public class AppConfig {
// all our beans (see next 2 sections)
}
Transaction Manager
First, we register the TransactionManager
database middleware. Spring Data Neo4J offers a tidy abstraction that takes care of all transaction management methods; imperative to pass the reactiveTransactionManager
name for our future reactive CRUD service to find it.
@Bean(name = "reactiveTransactionManager")
public ReactiveNeo4jTransactionManager reactiveTransactionManager(Driver driver,
ReactiveDatabaseSelectionProvider dbNameProvider) {
return new ReactiveNeo4jTransactionManager(driver, dbNameProvider);
}
We'll come back to the reactivity concept later on in this post.
Putting Cypher in place
βYou know, I know this steak doesn't exist. I know that when I put it in my mouth, the Matrix is telling my brain that it is juicy and delicious.β -- Cypher
@Bean
public Configuration cypherDslConfiguration() {
return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig()
.withDialect(Dialect.NEO4J_5).build();
}
With the current Spring Data Neo4J version (7.3.4
), CypherDSL will fall back to an older dialect that will, among other things, use the deprecated Long Id
field instead of the superseding String elementId
. We'll need to tell SPN here explicitly to use the latest dialect compatible with the Neo4J version >5
that we're running.
Models
Alright, now that we should have a running database service with a working application interface, we can start getting creative with our data layer primitives.
Let's have e.g. a Book
and an Author
model in src/main/java/db/model/*
@Data
@Node
public class Book {
@Id
@GeneratedValue
private String id;
private final String title;
private final String description;
@Relationship
private Author author;
}
@Node
binds our Book
POJO to a corresponding entity (aka node) in our graph data model. @Node
is conceptually the same as @Entity
if you're coming from the JPA paradigm.
@Relationship
lets us access connected nodes in our NoSQL graph model via relation properties.
@Data
@Node
public class Author {
@Id
@GeneratedValue
private String id;
private final String fullName;
}
@Data
comes with Lombok. Lombok is awesome: it delves under the hood for us and creates a constructor, getter & setter, hashCode functions etc. for our DTOs. All it takes is literally: @Data
Note that if we didn't define our fields as final
vars, Lombok 'd create a constructor for ALL fields by default, which in case of a database-managed ID
field wouldn't necessarily be a desirable thing...
3. Reactive CRUD Repo
We'll use a repository to manage all our Book
model interaction methods (i.e. find(All)?
, save(All)?
, delete(All)?
etc.) for us. Since we're mainly concerned with authors in terms of Book
properties, we'll spare ourselves a separate Author
repository.
public interface BookRepository extends ReactiveNeo4jRepository<Book, String> {
// add your custom CRUD methods
}
A word about reactive APIs
A reactive API relies on non-blocking callback-based IO-multiplexing. Now why would we want to have that?
Think optimized resource consumption: we wouldn't want to waste a whole thread on handling a single, potentially unreliable (e.g. high latency) connection synchronously (where there's a lot of requests queueing up).
Netty NIO framework is a popular implementation that forms β along with the Reactive Streams publish-subscribe callback specification β the basis for the Reactor high-level abstraction that comes with the Spring Framework.
With Reactor's Flux
and Mono
types as Publisher
implementations, sticking together asynchronous data processing pipelines is plug and play. But this comes at the expense that we cannot use it in the conventional imperative way as we would with synchronous blocking IO op procedure calls.
4. Sticking everything together
Great! Our src/main/java/*
packages so far:
.
βββ db
βΒ Β βββ DatabaseSeeder.javaβ
βΒ Β βββ model
| Β Β βββ Author.java β
βΒ Β βββ Book.java β
βββ org
βΒ Β βββ thematrix
| Β Β βββ library
| Β Β βββ AppConfig.java β
| Β Β βββ LibraryApplication.java β
βββ repos
βββ BookRepository.java β
Let's register our DatabaseSeeder class as a service provider to the Spring Container and autowire the BookRepository property to be automatically injected upon service start.
We'll have two methods down
and seed
attached to a context refresh event listener. down
and seed
will fire in the order of their definition once the application context starts or refreshes. Note that, to satisfy reactivity, stock.deleteAll()
shall not be used imperatively since it merely registers itself as a callback event publisher to our reactive BookRepository
.
@Profile("dev")
@Service
public class DatabaseSeeder {
@Autowired
private final BookRepository stock = null;
@EventListener
public Mono<Void> down(ContextRefreshedEvent event) {
return stock.deleteAll();
}
@EventListener
public Flux<Book> seed(ContextRefreshedEvent event) {
Faker fake = new Faker();
List<Book> shelf = new ArrayList<Book>();
for (int i = 0; i < 5; i++) {
Book book = new Book(fake.book.title(), fake.lorem.paragraph());
book.setAuthor(new Author(fake.artist.name()));
shelf.add(book);
}
return stock.saveAll(shelf);
}
}
Make sure we only seed in dev
environment.
spring.active.profile=dev
Now to add the necessary AppConfig
and package paths to the application entrypoint in our main method:
@SpringBootApplication(scanBasePackages = { "db", "db.model" })
@Import(AppConfig.class)
public class LibraryApplication {
public static void main(String[] args) {
SpringApplication.run(LibraryApplication.class, args);
}
}
We're all set! If we now run our app, the database seeder service will automatically write dummy data to the database. Mission accomplished. Tech Lead's proud of you. And this is merely where the fun begins.
Further Reading
Spring boot boilerplate
https://docs.spring.io/spring-data/neo4j/reference/getting-started.html#create-spring-boot-project
https://start.spring.io/
https://github.com/spring-projects/spring-data-neo4j/blob/main/README.adoc#getting-started
https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html
Neo4J
https://neo4j.com/docs/operations-manual/current/docker/introduction/
https://hub.docker.com/_/neo4j
https://neo4j.com/docs/operations-manual/current/configuration/configuration-settings/#config_dbms.usage_report.enabled
https://neo4j.com/docs/operations-manual/current/database-internals/transaction-management/
https://community.neo4j.com/t/unable-to-create-a-relationship-with-save-method-of-neo4jrepository/62773/3
https://github.com/spring-projects/spring-data-neo4j/issues/2848
Reactive programming
https://developer.okta.com/blog/2018/09/21/reactive-programming-with-spring
https://www.reactive-streams.org/
https://github.com/reactor/reactor-core/
https://github.com/reactor/reactor-netty/
https://www.reactivemanifesto.org/
Top comments (0)