Update Neo4j extension to use Spring Data Neo4j. (#125)
Move away from the Neo4j OGM usage because Spring Data Neo4j fits better in the ecosystem, spring-batch is settled in. Previously there was no visible difference to users because Spring Data Neo4j was based on Neo4j OGM which isn't the case anymore.
This commit is contained in:
9
.github/workflows/spring-batch-neo4j.yml
vendored
9
.github/workflows/spring-batch-neo4j.yml
vendored
@@ -11,11 +11,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 1.8
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
- name: Build with Maven
|
||||
run: mvn -B package --file pom.xml
|
||||
working-directory: spring-batch-neo4j
|
||||
|
||||
@@ -7,16 +7,11 @@ This extension contains an `ItemReader` and `ItemWriter` implementations for [Ne
|
||||
The `Neo4jItemReader` can be configured as follows:
|
||||
|
||||
```java
|
||||
SessionFactory sessionFactory = ...
|
||||
Neo4jItemReader<String> itemReader = new Neo4jItemReaderBuilder<String>()
|
||||
.sessionFactory(sessionFactory)
|
||||
.name("itemReader")
|
||||
.targetType(String.class)
|
||||
.startStatement("n=node(*)")
|
||||
.orderByStatement("n.age")
|
||||
.matchStatement("n -- m")
|
||||
.whereStatement("has(n.name)")
|
||||
.returnStatement("m")
|
||||
Neo4jItemReader<User> reader = new Neo4jItemReaderBuilder<User>()
|
||||
.neo4jTemplate(neo4jTemplate)
|
||||
.name("userReader")
|
||||
.statement(Cypher.match(userNode).returning(userNode))
|
||||
.targetType(User.class)
|
||||
.pageSize(50)
|
||||
.build();
|
||||
```
|
||||
@@ -24,8 +19,93 @@ Neo4jItemReader<String> itemReader = new Neo4jItemReaderBuilder<String>()
|
||||
The `Neo4jItemWriter` can be configured as follows:
|
||||
|
||||
```java
|
||||
SessionFactory sessionFactory = ...
|
||||
Neo4jItemWriter<String> writer = new Neo4jItemWriterBuilder<String>()
|
||||
.sessionFactory(sessionFactory)
|
||||
Neo4jItemWriter<User> writer = new Neo4jItemWriterBuilder<User>()
|
||||
.neo4jTemplate(neo4jTemplate)
|
||||
.neo4jDriver(driver)
|
||||
.neo4jMappingContext(mappingContext)
|
||||
.build();
|
||||
```
|
||||
|
||||
## Minimal Spring Boot example
|
||||
|
||||
Additional to the already existing dependencies in a new Spring Boot application,
|
||||
`spring-boot-starter-data-neo4j`, `spring-batch-neo4j` and the `spring-boot-starter-batch` are needed
|
||||
but `spring-jdbc` and `spring-boot-starter-jdbc` must be explicitly excluded.
|
||||
The exclusions are mandatory to avoid any need for JDBC-based connections, like JDBC URI etc.
|
||||
|
||||
See the following _build.gradle_ dependency definition for a minimal example.
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation ('org.springframework.boot:spring-boot-starter-batch') {
|
||||
exclude group: 'org.springframework', module: 'spring-jdbc'
|
||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-jdbc'
|
||||
}
|
||||
// current development version 0.2.0-SNAPSHOT
|
||||
implementation 'org.springframework.batch.extensions:spring-batch-neo4j'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'org.springframework.batch:spring-batch-test'
|
||||
}
|
||||
```
|
||||
|
||||
An example of the usage can be seen in the following example, implementing the `CommandLineRunner` interface.
|
||||
|
||||
```java
|
||||
@SpringBootApplication
|
||||
public class TestSpringBatchApplication implements CommandLineRunner {
|
||||
// those dependencies are created by Spring Boot's
|
||||
// spring-data-neo4j autoconfiguration
|
||||
@Autowired
|
||||
private Driver driver;
|
||||
@Autowired
|
||||
private Neo4jMappingContext mappingContext;
|
||||
@Autowired
|
||||
private Neo4jTemplate neo4jTemplate;
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(TestSpringBatchApplication.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
// writing
|
||||
Neo4jItemWriter<User> writer = new Neo4jItemWriterBuilder<User>()
|
||||
.neo4jTemplate(neo4jTemplate)
|
||||
.neo4jDriver(driver)
|
||||
.neo4jMappingContext(mappingContext)
|
||||
.build();
|
||||
writer.write(Chunk.of(new User("id1", "ab"), new User("id2", "bb")));
|
||||
|
||||
// reading
|
||||
org.neo4j.cypherdsl.core.Node userNode = Cypher.node("User");
|
||||
Neo4jItemReader<User> reader = new Neo4jItemReaderBuilder<User>()
|
||||
.neo4jTemplate(neo4jTemplate)
|
||||
.name("userReader")
|
||||
.statement(Cypher.match(userNode).returning(userNode))
|
||||
.targetType(User.class)
|
||||
.build();
|
||||
List<User> allUsers = new ArrayList<>();
|
||||
User user = null;
|
||||
while ((user = reader.read()) != null) {
|
||||
System.out.printf("Found user: %s%n", user.name);
|
||||
allUsers.add(user);
|
||||
}
|
||||
|
||||
// deleting
|
||||
writer.setDelete(true);
|
||||
writer.write(Chunk.of(allUsers.toArray(new User[]{})));
|
||||
}
|
||||
|
||||
@Node("User")
|
||||
public static class User {
|
||||
@Id public final String id;
|
||||
public final String name;
|
||||
|
||||
public User(String id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -54,19 +54,19 @@
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>1.8</java.version>
|
||||
<java.version>17</java.version>
|
||||
|
||||
<!-- Production dependencies-->
|
||||
<spring.batch.version>4.3.3</spring.batch.version>
|
||||
<neo4j-ogm-core.version>3.2.21</neo4j-ogm-core.version>
|
||||
<spring.batch.version>5.1.2</spring.batch.version>
|
||||
<spring-data-neo4j.version>7.2.1</spring-data-neo4j.version>
|
||||
|
||||
<!-- Test Dependencies -->
|
||||
<assertj.version>3.18.1</assertj.version>
|
||||
<junit.version>4.13.2</junit.version>
|
||||
<mockito.version>3.6.0</mockito.version>
|
||||
<junit.version>5.11.0</junit.version>
|
||||
<mockito.version>5.12.0</mockito.version>
|
||||
|
||||
<!-- Maven plugins -->
|
||||
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
|
||||
<maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
|
||||
<maven-javadoc-plugin.version>3.2.0</maven-javadoc-plugin.version>
|
||||
<maven-source-plugin.version>3.2.1</maven-source-plugin.version>
|
||||
</properties>
|
||||
@@ -83,15 +83,15 @@
|
||||
<version>${spring.batch.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.neo4j</groupId>
|
||||
<artifactId>neo4j-ogm-core</artifactId>
|
||||
<version>${neo4j-ogm-core.version}</version>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-neo4j</artifactId>
|
||||
<version>${spring-data-neo4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test Dependencies -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -16,20 +16,19 @@
|
||||
|
||||
package org.springframework.batch.extensions.neo4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.neo4j.ogm.session.Session;
|
||||
import org.neo4j.ogm.session.SessionFactory;
|
||||
|
||||
import org.neo4j.cypherdsl.core.Statement;
|
||||
import org.neo4j.cypherdsl.core.StatementBuilder;
|
||||
import org.neo4j.cypherdsl.core.renderer.Renderer;
|
||||
import org.springframework.batch.item.ItemReader;
|
||||
import org.springframework.batch.item.data.AbstractPaginatedDataItemReader;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.data.neo4j.core.Neo4jTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -38,7 +37,7 @@ import org.springframework.util.StringUtils;
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* It executes cypher queries built from the statement fragments provided to
|
||||
* It executes cypher queries built from the statement provided to
|
||||
* retrieve the requested data. The query is executed using paged requests of
|
||||
* a size specified in {@link #setPageSize(int)}. Additional pages are requested
|
||||
* as needed when the {@link #read()} method is called. On restart, the reader
|
||||
@@ -46,7 +45,7 @@ import org.springframework.util.StringUtils;
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Performance is dependent on your Neo4J configuration (embedded or remote) as
|
||||
* Performance is dependent on your Neo4j configuration as
|
||||
* well as page size. Setting a fairly large page size and using a commit
|
||||
* interval that matches the page size should provide better performance.
|
||||
* </p>
|
||||
@@ -58,167 +57,87 @@ import org.springframework.util.StringUtils;
|
||||
* environment (no restart available).
|
||||
* </p>
|
||||
*
|
||||
* @param <T> type of entity to load
|
||||
* @author Michael Minella
|
||||
* @author Mahmoud Ben Hassine
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
public class Neo4jItemReader<T> extends AbstractPaginatedDataItemReader<T> implements InitializingBean {
|
||||
|
||||
protected Log logger = LogFactory.getLog(getClass());
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private SessionFactory sessionFactory;
|
||||
private Neo4jTemplate neo4jTemplate;
|
||||
|
||||
private String startStatement;
|
||||
private String returnStatement;
|
||||
private String matchStatement;
|
||||
private String whereStatement;
|
||||
private String orderByStatement;
|
||||
private StatementBuilder.OngoingReadingAndReturn statement;
|
||||
|
||||
private Class<T> targetType;
|
||||
private Class<T> targetType;
|
||||
|
||||
private Map<String, Object> parameterValues;
|
||||
private Map<String, Object> parameterValues;
|
||||
|
||||
/**
|
||||
* Optional parameters to be used in the cypher query.
|
||||
*
|
||||
* @param parameterValues the parameter values to be used in the cypher query
|
||||
*/
|
||||
public void setParameterValues(Map<String, Object> parameterValues) {
|
||||
this.parameterValues = parameterValues;
|
||||
}
|
||||
/**
|
||||
* Optional parameters to be used in the cypher query.
|
||||
*
|
||||
* @param parameterValues the parameter values to be used in the cypher query
|
||||
*/
|
||||
public void setParameterValues(Map<String, Object> parameterValues) {
|
||||
this.parameterValues = parameterValues;
|
||||
}
|
||||
|
||||
protected final Map<String, Object> getParameterValues() {
|
||||
return this.parameterValues;
|
||||
}
|
||||
/**
|
||||
* Cypher-DSL's {@link org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingAndReturn} statement
|
||||
* without skip and limit segments. Those will get added by the pagination mechanism later.
|
||||
*
|
||||
* @param statement the Cypher-DSL statement-in-construction.
|
||||
*/
|
||||
public void setStatement(StatementBuilder.OngoingReadingAndReturn statement) {
|
||||
this.statement = statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* The start segment of the cypher query. START is prepended
|
||||
* to the statement provided and should <em>not</em> be
|
||||
* included.
|
||||
*
|
||||
* @param startStatement the start fragment of the cypher query.
|
||||
*/
|
||||
public void setStartStatement(String startStatement) {
|
||||
this.startStatement = startStatement;
|
||||
}
|
||||
/**
|
||||
* Establish the Neo4jTemplate for the reader.
|
||||
*
|
||||
* @param neo4jTemplate the template to use for the reader.
|
||||
*/
|
||||
public void setNeo4jTemplate(Neo4jTemplate neo4jTemplate) {
|
||||
this.neo4jTemplate = neo4jTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* The return statement of the cypher query. RETURN is prepended
|
||||
* to the statement provided and should <em>not</em> be
|
||||
* included
|
||||
*
|
||||
* @param returnStatement the return fragment of the cypher query.
|
||||
*/
|
||||
public void setReturnStatement(String returnStatement) {
|
||||
this.returnStatement = returnStatement;
|
||||
}
|
||||
/**
|
||||
* The object type to be returned from each call to {@link #read()}
|
||||
*
|
||||
* @param targetType the type of object to return.
|
||||
*/
|
||||
public void setTargetType(Class<T> targetType) {
|
||||
this.targetType = targetType;
|
||||
}
|
||||
|
||||
/**
|
||||
* An optional match fragment of the cypher query. MATCH is
|
||||
* prepended to the statement provided and should <em>not</em>
|
||||
* be included.
|
||||
*
|
||||
* @param matchStatement the match fragment of the cypher query
|
||||
*/
|
||||
public void setMatchStatement(String matchStatement) {
|
||||
this.matchStatement = matchStatement;
|
||||
}
|
||||
private Statement generateStatement() {
|
||||
Statement builtStatement = statement
|
||||
.skip(page * pageSize)
|
||||
.limit(pageSize)
|
||||
.build();
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(Renderer.getDefaultRenderer().render(builtStatement));
|
||||
}
|
||||
|
||||
/**
|
||||
* An optional where fragment of the cypher query. WHERE is
|
||||
* prepended to the statement provided and should <em>not</em>
|
||||
* be included.
|
||||
*
|
||||
* @param whereStatement where fragment of the cypher query
|
||||
*/
|
||||
public void setWhereStatement(String whereStatement) {
|
||||
this.whereStatement = whereStatement;
|
||||
}
|
||||
return builtStatement;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of properties to order the results by. This is
|
||||
* required so that subsequent page requests pull back the
|
||||
* segment of results correctly. ORDER BY is prepended to
|
||||
* the statement provided and should <em>not</em> be included.
|
||||
*
|
||||
* @param orderByStatement order by fragment of the cypher query.
|
||||
*/
|
||||
public void setOrderByStatement(String orderByStatement) {
|
||||
this.orderByStatement = orderByStatement;
|
||||
}
|
||||
/**
|
||||
* Checks mandatory properties
|
||||
*
|
||||
* @see InitializingBean#afterPropertiesSet()
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
Assert.state(neo4jTemplate != null, "A Neo4jTemplate is required");
|
||||
Assert.state(targetType != null, "The type to be returned is required");
|
||||
Assert.state(statement != null, "A statement is required");
|
||||
}
|
||||
|
||||
protected SessionFactory getSessionFactory() {
|
||||
return sessionFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish the session factory for the reader.
|
||||
* @param sessionFactory the factory to use for the reader.
|
||||
*/
|
||||
public void setSessionFactory(SessionFactory sessionFactory) {
|
||||
this.sessionFactory = sessionFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* The object type to be returned from each call to {@link #read()}
|
||||
*
|
||||
* @param targetType the type of object to return.
|
||||
*/
|
||||
public void setTargetType(Class<T> targetType) {
|
||||
this.targetType = targetType;
|
||||
}
|
||||
|
||||
protected final Class<T> getTargetType() {
|
||||
return this.targetType;
|
||||
}
|
||||
|
||||
protected String generateLimitCypherQuery() {
|
||||
StringBuilder query = new StringBuilder(128);
|
||||
|
||||
query.append("START ").append(startStatement);
|
||||
query.append(matchStatement != null ? " MATCH " + matchStatement : "");
|
||||
query.append(whereStatement != null ? " WHERE " + whereStatement : "");
|
||||
query.append(" RETURN ").append(returnStatement);
|
||||
query.append(" ORDER BY ").append(orderByStatement);
|
||||
query.append(" SKIP " + (pageSize * page));
|
||||
query.append(" LIMIT " + pageSize);
|
||||
|
||||
String resultingQuery = query.toString();
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(resultingQuery);
|
||||
}
|
||||
|
||||
return resultingQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks mandatory properties
|
||||
*
|
||||
* @see InitializingBean#afterPropertiesSet()
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Assert.state(sessionFactory != null,"A SessionFactory is required");
|
||||
Assert.state(targetType != null, "The type to be returned is required");
|
||||
Assert.state(StringUtils.hasText(startStatement), "A START statement is required");
|
||||
Assert.state(StringUtils.hasText(returnStatement), "A RETURN statement is required");
|
||||
Assert.state(StringUtils.hasText(orderByStatement), "A ORDER BY statement is required");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
protected Iterator<T> doPageRead() {
|
||||
Session session = getSessionFactory().openSession();
|
||||
|
||||
Iterable<T> queryResults = session.query(getTargetType(),
|
||||
generateLimitCypherQuery(),
|
||||
getParameterValues());
|
||||
|
||||
if(queryResults != null) {
|
||||
return queryResults.iterator();
|
||||
}
|
||||
else {
|
||||
return new ArrayList<T>().iterator();
|
||||
}
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
protected Iterator<T> doPageRead() {
|
||||
return neo4jTemplate.findAll(generateStatement(), parameterValues, targetType).iterator();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,17 +16,22 @@
|
||||
|
||||
package org.springframework.batch.extensions.neo4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.neo4j.ogm.session.Session;
|
||||
import org.neo4j.ogm.session.SessionFactory;
|
||||
|
||||
import org.neo4j.cypherdsl.core.Cypher;
|
||||
import org.neo4j.cypherdsl.core.Node;
|
||||
import org.neo4j.cypherdsl.core.Statement;
|
||||
import org.neo4j.cypherdsl.core.renderer.Renderer;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.springframework.batch.item.Chunk;
|
||||
import org.springframework.batch.item.ItemWriter;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.data.neo4j.core.Neo4jTemplate;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -38,90 +43,114 @@ import org.springframework.util.CollectionUtils;
|
||||
* behavior) so it can be used in multiple concurrent transactions.
|
||||
* </p>
|
||||
*
|
||||
* @param <T> type of the entity to write
|
||||
* @author Michael Minella
|
||||
* @author Glenn Renfro
|
||||
* @author Mahmoud Ben Hassine
|
||||
*
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
public class Neo4jItemWriter<T> implements ItemWriter<T>, InitializingBean {
|
||||
|
||||
protected static final Log logger = LogFactory
|
||||
.getLog(Neo4jItemWriter.class);
|
||||
private boolean delete = false;
|
||||
|
||||
private boolean delete = false;
|
||||
private Neo4jTemplate neo4jTemplate;
|
||||
private Neo4jMappingContext neo4jMappingContext;
|
||||
private Driver neo4jDriver;
|
||||
|
||||
private SessionFactory sessionFactory;
|
||||
/**
|
||||
* Boolean flag indicating whether the writer should save or delete the item at write
|
||||
* time.
|
||||
*
|
||||
* @param delete true if write should delete item, false if item should be saved.
|
||||
* Default is false.
|
||||
*/
|
||||
public void setDelete(boolean delete) {
|
||||
this.delete = delete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Boolean flag indicating whether the writer should save or delete the item at write
|
||||
* time.
|
||||
* @param delete true if write should delete item, false if item should be saved.
|
||||
* Default is false.
|
||||
*/
|
||||
public void setDelete(boolean delete) {
|
||||
this.delete = delete;
|
||||
}
|
||||
/**
|
||||
* Establish the neo4jTemplate for interacting with Neo4j.
|
||||
*
|
||||
* @param neo4jTemplate neo4jTemplate to be used.
|
||||
*/
|
||||
public void setNeo4jTemplate(Neo4jTemplate neo4jTemplate) {
|
||||
this.neo4jTemplate = neo4jTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish the session factory that will be used to create {@link Session} instances
|
||||
* for interacting with Neo4j.
|
||||
* @param sessionFactory sessionFactory to be used.
|
||||
*/
|
||||
public void setSessionFactory(SessionFactory sessionFactory) {
|
||||
this.sessionFactory = sessionFactory;
|
||||
}
|
||||
/**
|
||||
* Set the Neo4j driver to be used for the delete operation
|
||||
*
|
||||
* @param neo4jDriver configured Neo4j driver instance
|
||||
*/
|
||||
public void setNeo4jDriver(Driver neo4jDriver) {
|
||||
this.neo4jDriver = neo4jDriver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks mandatory properties
|
||||
*
|
||||
* @see InitializingBean#afterPropertiesSet()
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Assert.state(this.sessionFactory != null,
|
||||
"A SessionFactory is required");
|
||||
}
|
||||
/**
|
||||
* Neo4jMappingContext needed for determine the id type of the entity instances.
|
||||
*
|
||||
* @param neo4jMappingContext initialized mapping context
|
||||
*/
|
||||
public void setNeo4jMappingContext(Neo4jMappingContext neo4jMappingContext) {
|
||||
this.neo4jMappingContext = neo4jMappingContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write all items to the data store.
|
||||
*
|
||||
* @see org.springframework.batch.item.ItemWriter#write(java.util.List)
|
||||
*/
|
||||
@Override
|
||||
public void write(List<? extends T> items) throws Exception {
|
||||
if(!CollectionUtils.isEmpty(items)) {
|
||||
doWrite(items);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Checks mandatory properties
|
||||
*
|
||||
* @see InitializingBean#afterPropertiesSet()
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
Assert.state(this.neo4jTemplate != null, "A Neo4jTemplate is required");
|
||||
Assert.state(this.neo4jMappingContext != null, "A Neo4jMappingContext is required");
|
||||
Assert.state(this.neo4jDriver != null, "A Neo4j driver is required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the actual write using the template. This can be overridden by
|
||||
* a subclass if necessary.
|
||||
*
|
||||
* @param items the list of items to be persisted.
|
||||
*/
|
||||
protected void doWrite(List<? extends T> items) {
|
||||
if(delete) {
|
||||
delete(items);
|
||||
}
|
||||
else {
|
||||
save(items);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Write all items to the data store.
|
||||
*
|
||||
* @see org.springframework.batch.item.ItemWriter#write(Chunk chunk)
|
||||
*/
|
||||
@Override
|
||||
public void write(@NonNull Chunk<? extends T> chunk) {
|
||||
if (!chunk.isEmpty()) {
|
||||
doWrite(chunk.getItems());
|
||||
}
|
||||
}
|
||||
|
||||
private void delete(List<? extends T> items) {
|
||||
Session session = this.sessionFactory.openSession();
|
||||
/**
|
||||
* Performs the actual write using the template. This can be overridden by
|
||||
* a subclass if necessary.
|
||||
*
|
||||
* @param items the list of items to be persisted.
|
||||
*/
|
||||
protected void doWrite(List<? extends T> items) {
|
||||
if (delete) {
|
||||
delete(items);
|
||||
} else {
|
||||
save(items);
|
||||
}
|
||||
}
|
||||
|
||||
for(T item : items) {
|
||||
session.delete(item);
|
||||
}
|
||||
}
|
||||
private void delete(List<? extends T> items) {
|
||||
for (T item : items) {
|
||||
// Figure out id field individually because different
|
||||
// id strategies could have been taken for classes within a
|
||||
// business model hierarchy.
|
||||
Neo4jPersistentEntity<?> nodeDescription = (Neo4jPersistentEntity<?>) this.neo4jMappingContext.getNodeDescription(item.getClass());
|
||||
Object identifier = nodeDescription.getIdentifierAccessor(item).getRequiredIdentifier();
|
||||
Node named = Cypher.anyNode().named(nodeDescription.getPrimaryLabel());
|
||||
Statement statement = Cypher.match(named)
|
||||
.where(nodeDescription.getIdDescription().asIdExpression(nodeDescription.getPrimaryLabel()).eq(Cypher.parameter("id")))
|
||||
.detachDelete(named).build();
|
||||
|
||||
private void save(List<? extends T> items) {
|
||||
Session session = this.sessionFactory.openSession();
|
||||
String renderedStatement = Renderer.getDefaultRenderer().render(statement);
|
||||
this.neo4jDriver.executableQuery(renderedStatement).withParameters(Map.of("id", identifier)).execute();
|
||||
}
|
||||
}
|
||||
|
||||
for (T item : items) {
|
||||
session.save(item);
|
||||
}
|
||||
}
|
||||
private void save(List<? extends T> items) {
|
||||
this.neo4jTemplate.saveAll(items);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,258 +16,190 @@
|
||||
|
||||
package org.springframework.batch.extensions.neo4j.builder;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.neo4j.ogm.session.SessionFactory;
|
||||
|
||||
import org.neo4j.cypherdsl.core.StatementBuilder;
|
||||
import org.springframework.batch.extensions.neo4j.Neo4jItemReader;
|
||||
import org.springframework.data.neo4j.core.Neo4jTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A builder for the {@link Neo4jItemReader}.
|
||||
*
|
||||
* @param <T> type of the entity to read
|
||||
* @author Glenn Renfro
|
||||
* @author Gerrit Meier
|
||||
* @see Neo4jItemReader
|
||||
*/
|
||||
public class Neo4jItemReaderBuilder<T> {
|
||||
|
||||
private SessionFactory sessionFactory;
|
||||
private Neo4jTemplate neo4jTemplate;
|
||||
|
||||
private String startStatement;
|
||||
private StatementBuilder.OngoingReadingAndReturn statement;
|
||||
|
||||
private String returnStatement;
|
||||
private Class<T> targetType;
|
||||
|
||||
private String matchStatement;
|
||||
private Map<String, Object> parameterValues;
|
||||
|
||||
private String whereStatement;
|
||||
private int pageSize = 10;
|
||||
|
||||
private String orderByStatement;
|
||||
private boolean saveState = true;
|
||||
|
||||
private Class<T> targetType;
|
||||
private String name;
|
||||
|
||||
private Map<String, Object> parameterValues;
|
||||
private int maxItemCount = Integer.MAX_VALUE;
|
||||
|
||||
private int pageSize = 10;
|
||||
private int currentItemCount;
|
||||
|
||||
private boolean saveState = true;
|
||||
/**
|
||||
* Configure if the state of the {@link org.springframework.batch.item.ItemStreamSupport}
|
||||
* should be persisted within the {@link org.springframework.batch.item.ExecutionContext}
|
||||
* for restart purposes.
|
||||
*
|
||||
* @param saveState defaults to true
|
||||
* @return The current instance of the builder.
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> saveState(boolean saveState) {
|
||||
this.saveState = saveState;
|
||||
|
||||
private String name;
|
||||
return this;
|
||||
}
|
||||
|
||||
private int maxItemCount = Integer.MAX_VALUE;
|
||||
/**
|
||||
* The name used to calculate the key within the
|
||||
* {@link org.springframework.batch.item.ExecutionContext}. Required if
|
||||
* {@link #saveState(boolean)} is set to true.
|
||||
*
|
||||
* @param name name of the reader instance
|
||||
* @return The current instance of the builder.
|
||||
* @see org.springframework.batch.item.ItemStreamSupport#setName(String)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> name(String name) {
|
||||
this.name = name;
|
||||
|
||||
private int currentItemCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure if the state of the {@link org.springframework.batch.item.ItemStreamSupport}
|
||||
* should be persisted within the {@link org.springframework.batch.item.ExecutionContext}
|
||||
* for restart purposes.
|
||||
*
|
||||
* @param saveState defaults to true
|
||||
* @return The current instance of the builder.
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> saveState(boolean saveState) {
|
||||
this.saveState = saveState;
|
||||
/**
|
||||
* Configure the max number of items to be read.
|
||||
*
|
||||
* @param maxItemCount the max items to be read
|
||||
* @return The current instance of the builder.
|
||||
* @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setMaxItemCount(int)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> maxItemCount(int maxItemCount) {
|
||||
this.maxItemCount = maxItemCount;
|
||||
|
||||
return this;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name used to calculate the key within the
|
||||
* {@link org.springframework.batch.item.ExecutionContext}. Required if
|
||||
* {@link #saveState(boolean)} is set to true.
|
||||
*
|
||||
* @param name name of the reader instance
|
||||
* @return The current instance of the builder.
|
||||
* @see org.springframework.batch.item.ItemStreamSupport#setName(String)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> name(String name) {
|
||||
this.name = name;
|
||||
/**
|
||||
* Index for the current item. Used on restarts to indicate where to start from.
|
||||
*
|
||||
* @param currentItemCount current index
|
||||
* @return this instance for method chaining
|
||||
* @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setCurrentItemCount(int)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> currentItemCount(int currentItemCount) {
|
||||
this.currentItemCount = currentItemCount;
|
||||
|
||||
return this;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the max number of items to be read.
|
||||
*
|
||||
* @param maxItemCount the max items to be read
|
||||
* @return The current instance of the builder.
|
||||
* @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setMaxItemCount(int)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> maxItemCount(int maxItemCount) {
|
||||
this.maxItemCount = maxItemCount;
|
||||
/**
|
||||
* Establish the neo4jTemplate for the reader.
|
||||
*
|
||||
* @param neo4jTemplate the template to use for the reader.
|
||||
* @return this instance for method chaining
|
||||
* @see Neo4jItemReader#setNeo4jTemplate(Neo4jTemplate)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> neo4jTemplate(Neo4jTemplate neo4jTemplate) {
|
||||
this.neo4jTemplate = neo4jTemplate;
|
||||
|
||||
return this;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Index for the current item. Used on restarts to indicate where to start from.
|
||||
*
|
||||
* @param currentItemCount current index
|
||||
* @return this instance for method chaining
|
||||
* @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setCurrentItemCount(int)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> currentItemCount(int currentItemCount) {
|
||||
this.currentItemCount = currentItemCount;
|
||||
/**
|
||||
* The number of items to be read with each page.
|
||||
*
|
||||
* @param pageSize the number of items
|
||||
* @return this instance for method chaining
|
||||
* @see Neo4jItemReader#setPageSize(int)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> pageSize(int pageSize) {
|
||||
this.pageSize = pageSize;
|
||||
|
||||
return this;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish the session factory for the reader.
|
||||
* @param sessionFactory the factory to use for the reader.
|
||||
* @return this instance for method chaining
|
||||
* @see Neo4jItemReader#setSessionFactory(SessionFactory)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> sessionFactory(SessionFactory sessionFactory) {
|
||||
this.sessionFactory = sessionFactory;
|
||||
/**
|
||||
* Optional parameters to be used in the cypher query.
|
||||
*
|
||||
* @param parameterValues the parameter values to be used in the cypher query
|
||||
* @return this instance for method chaining
|
||||
* @see Neo4jItemReader#setParameterValues(Map)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> parameterValues(Map<String, Object> parameterValues) {
|
||||
this.parameterValues = parameterValues;
|
||||
|
||||
return this;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of items to be read with each page.
|
||||
*
|
||||
* @param pageSize the number of items
|
||||
* @return this instance for method chaining
|
||||
* @see Neo4jItemReader#setPageSize(int)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> pageSize(int pageSize) {
|
||||
this.pageSize = pageSize;
|
||||
/**
|
||||
* Cypher-DSL's {@link org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingAndReturn} statement
|
||||
* without skip and limit segments. Those will get added by the pagination mechanism later.
|
||||
*
|
||||
* @param statement the cypher query without SKIP or LIMIT
|
||||
* @return this instance for method chaining
|
||||
* @see Neo4jItemReader#setStatement(org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingAndReturn)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> statement(StatementBuilder.OngoingReadingAndReturn statement) {
|
||||
this.statement = statement;
|
||||
|
||||
return this;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional parameters to be used in the cypher query.
|
||||
*
|
||||
* @param parameterValues the parameter values to be used in the cypher query
|
||||
* @return this instance for method chaining
|
||||
* @see Neo4jItemReader#setParameterValues(Map)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> parameterValues(Map<String, Object> parameterValues) {
|
||||
this.parameterValues = parameterValues;
|
||||
/**
|
||||
* The object type to be returned from each call to {@link Neo4jItemReader#read()}
|
||||
*
|
||||
* @param targetType the type of object to return.
|
||||
* @return this instance for method chaining
|
||||
* @see Neo4jItemReader#setTargetType(Class)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> targetType(Class<T> targetType) {
|
||||
this.targetType = targetType;
|
||||
|
||||
return this;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The start segment of the cypher query. START is prepended to the statement provided
|
||||
* and should <em>not</em> be included.
|
||||
*
|
||||
* @param startStatement the start fragment of the cypher query.
|
||||
* @return this instance for method chaining
|
||||
* @see Neo4jItemReader#setStartStatement(String)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> startStatement(String startStatement) {
|
||||
this.startStatement = startStatement;
|
||||
/**
|
||||
* Returns a fully constructed {@link Neo4jItemReader}.
|
||||
*
|
||||
* @return a new {@link Neo4jItemReader}
|
||||
*/
|
||||
public Neo4jItemReader<T> build() {
|
||||
if (this.saveState) {
|
||||
Assert.hasText(this.name, "A name is required when saveState is set to true");
|
||||
}
|
||||
Assert.notNull(this.neo4jTemplate, "neo4jTemplate is required.");
|
||||
Assert.notNull(this.targetType, "targetType is required.");
|
||||
Assert.notNull(this.statement, "statement is required.");
|
||||
Assert.isTrue(this.pageSize > 0, "pageSize must be greater than zero");
|
||||
Assert.isTrue(this.maxItemCount > 0, "maxItemCount must be greater than zero");
|
||||
Assert.isTrue(this.maxItemCount > this.currentItemCount, "maxItemCount must be greater than currentItemCount");
|
||||
|
||||
return this;
|
||||
}
|
||||
Neo4jItemReader<T> reader = new Neo4jItemReader<>();
|
||||
reader.setPageSize(this.pageSize);
|
||||
reader.setParameterValues(this.parameterValues);
|
||||
reader.setNeo4jTemplate(this.neo4jTemplate);
|
||||
reader.setTargetType(this.targetType);
|
||||
reader.setStatement(this.statement);
|
||||
reader.setName(this.name);
|
||||
reader.setSaveState(this.saveState);
|
||||
reader.setCurrentItemCount(this.currentItemCount);
|
||||
reader.setMaxItemCount(this.maxItemCount);
|
||||
|
||||
/**
|
||||
* The return statement of the cypher query. RETURN is prepended to the statement
|
||||
* provided and should <em>not</em> be included
|
||||
*
|
||||
* @param returnStatement the return fragment of the cypher query.
|
||||
* @return this instance for method chaining
|
||||
* @see Neo4jItemReader#setReturnStatement(String)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> returnStatement(String returnStatement) {
|
||||
this.returnStatement = returnStatement;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* An optional match fragment of the cypher query. MATCH is prepended to the statement
|
||||
* provided and should <em>not</em> be included.
|
||||
*
|
||||
* @param matchStatement the match fragment of the cypher query
|
||||
* @return this instance for method chaining
|
||||
* @see Neo4jItemReader#setMatchStatement(String)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> matchStatement(String matchStatement) {
|
||||
this.matchStatement = matchStatement;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* An optional where fragment of the cypher query. WHERE is prepended to the statement
|
||||
* provided and should <em>not</em> be included.
|
||||
*
|
||||
* @param whereStatement where fragment of the cypher query
|
||||
* @return this instance for method chaining
|
||||
* @see Neo4jItemReader#setWhereStatement(String)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> whereStatement(String whereStatement) {
|
||||
this.whereStatement = whereStatement;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of properties to order the results by. This is required so that subsequent
|
||||
* page requests pull back the segment of results correctly. ORDER BY is prepended to
|
||||
* the statement provided and should <em>not</em> be included.
|
||||
*
|
||||
* @param orderByStatement order by fragment of the cypher query.
|
||||
* @return this instance for method chaining
|
||||
* @see Neo4jItemReader#setOrderByStatement(String)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> orderByStatement(String orderByStatement) {
|
||||
this.orderByStatement = orderByStatement;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The object type to be returned from each call to {@link Neo4jItemReader#read()}
|
||||
*
|
||||
* @param targetType the type of object to return.
|
||||
* @return this instance for method chaining
|
||||
* @see Neo4jItemReader#setTargetType(Class)
|
||||
*/
|
||||
public Neo4jItemReaderBuilder<T> targetType(Class<T> targetType) {
|
||||
this.targetType = targetType;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fully constructed {@link Neo4jItemReader}.
|
||||
*
|
||||
* @return a new {@link Neo4jItemReader}
|
||||
*/
|
||||
public Neo4jItemReader<T> build() {
|
||||
if (this.saveState) {
|
||||
Assert.hasText(this.name, "A name is required when saveState is set to true");
|
||||
}
|
||||
Assert.notNull(this.sessionFactory, "sessionFactory is required.");
|
||||
Assert.notNull(this.targetType, "targetType is required.");
|
||||
Assert.hasText(this.startStatement, "startStatement is required.");
|
||||
Assert.hasText(this.returnStatement, "returnStatement is required.");
|
||||
Assert.hasText(this.orderByStatement, "orderByStatement is required.");
|
||||
Assert.isTrue(this.pageSize > 0, "pageSize must be greater than zero");
|
||||
Assert.isTrue(this.maxItemCount > 0, "maxItemCount must be greater than zero");
|
||||
Assert.isTrue(this.maxItemCount > this.currentItemCount , "maxItemCount must be greater than currentItemCount");
|
||||
|
||||
Neo4jItemReader<T> reader = new Neo4jItemReader<>();
|
||||
reader.setMatchStatement(this.matchStatement);
|
||||
reader.setOrderByStatement(this.orderByStatement);
|
||||
reader.setPageSize(this.pageSize);
|
||||
reader.setParameterValues(this.parameterValues);
|
||||
reader.setSessionFactory(this.sessionFactory);
|
||||
reader.setTargetType(this.targetType);
|
||||
reader.setStartStatement(this.startStatement);
|
||||
reader.setReturnStatement(this.returnStatement);
|
||||
reader.setWhereStatement(this.whereStatement);
|
||||
reader.setName(this.name);
|
||||
reader.setSaveState(this.saveState);
|
||||
reader.setCurrentItemCount(this.currentItemCount);
|
||||
reader.setMaxItemCount(this.maxItemCount);
|
||||
|
||||
return reader;
|
||||
}
|
||||
return reader;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/*
|
||||
* Copyright 2017-2021 the original author or authors.
|
||||
*
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
@@ -16,61 +16,91 @@
|
||||
|
||||
package org.springframework.batch.extensions.neo4j.builder;
|
||||
|
||||
import org.neo4j.ogm.session.Session;
|
||||
import org.neo4j.ogm.session.SessionFactory;
|
||||
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.springframework.batch.extensions.neo4j.Neo4jItemWriter;
|
||||
import org.springframework.data.neo4j.core.Neo4jTemplate;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A builder implementation for the {@link Neo4jItemWriter}
|
||||
*
|
||||
* @param <T> type of the entity to write
|
||||
* @author Glenn Renfro
|
||||
* @author Gerrit Meier
|
||||
* @see Neo4jItemWriter
|
||||
*/
|
||||
public class Neo4jItemWriterBuilder<T> {
|
||||
|
||||
private boolean delete = false;
|
||||
private boolean delete = false;
|
||||
|
||||
private SessionFactory sessionFactory;
|
||||
private Neo4jTemplate neo4jTemplate;
|
||||
private Driver neo4jDriver;
|
||||
private Neo4jMappingContext neo4jMappingContext;
|
||||
|
||||
/**
|
||||
* Boolean flag indicating whether the writer should save or delete the item at write
|
||||
* time.
|
||||
* @param delete true if write should delete item, false if item should be saved.
|
||||
* Default is false.
|
||||
* @return The current instance of the builder
|
||||
* @see Neo4jItemWriter#setDelete(boolean)
|
||||
*/
|
||||
public Neo4jItemWriterBuilder<T> delete(boolean delete) {
|
||||
this.delete = delete;
|
||||
/**
|
||||
* Boolean flag indicating whether the writer should save or delete the item at write
|
||||
* time.
|
||||
*
|
||||
* @param delete true if write should delete item, false if item should be saved.
|
||||
* Default is false.
|
||||
* @return The current instance of the builder
|
||||
* @see Neo4jItemWriter#setDelete(boolean)
|
||||
*/
|
||||
public Neo4jItemWriterBuilder<T> delete(boolean delete) {
|
||||
this.delete = delete;
|
||||
return this;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Establish the session factory that will be used to create {@link Neo4jTemplate} instances
|
||||
* for interacting with Neo4j.
|
||||
*
|
||||
* @param neo4jTemplate neo4jTemplate to be used.
|
||||
* @return The current instance of the builder
|
||||
* @see Neo4jItemWriter#setNeo4jTemplate(Neo4jTemplate)
|
||||
*/
|
||||
public Neo4jItemWriterBuilder<T> neo4jTemplate(Neo4jTemplate neo4jTemplate) {
|
||||
this.neo4jTemplate = neo4jTemplate;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish the session factory that will be used to create {@link Session} instances
|
||||
* for interacting with Neo4j.
|
||||
* @param sessionFactory sessionFactory to be used.
|
||||
* @return The current instance of the builder
|
||||
* @see Neo4jItemWriter#setSessionFactory(SessionFactory)
|
||||
*/
|
||||
public Neo4jItemWriterBuilder<T> sessionFactory(SessionFactory sessionFactory) {
|
||||
this.sessionFactory = sessionFactory;
|
||||
/**
|
||||
* Set the preconfigured Neo4j driver to be used within the built writer instance.
|
||||
*
|
||||
* @param neo4jDriver preconfigured Neo4j driver instance
|
||||
* @return The current instance of the builder
|
||||
*/
|
||||
public Neo4jItemWriterBuilder<T> neo4jDriver(Driver neo4jDriver) {
|
||||
this.neo4jDriver = neo4jDriver;
|
||||
return this;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Set the Neo4jMappingContext to be used within the built writer instance.
|
||||
*
|
||||
* @param neo4jMappingContext initialized Neo4jMappingContext instance
|
||||
* @return The current instance of the builder
|
||||
*/
|
||||
public Neo4jItemWriterBuilder<T> neo4jMappingContext(Neo4jMappingContext neo4jMappingContext) {
|
||||
this.neo4jMappingContext = neo4jMappingContext;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and builds a {@link org.springframework.batch.extensions.neo4j.Neo4jItemWriter}.
|
||||
*
|
||||
* @return a {@link Neo4jItemWriter}
|
||||
*/
|
||||
public Neo4jItemWriter<T> build() {
|
||||
Assert.notNull(sessionFactory, "sessionFactory is required.");
|
||||
Neo4jItemWriter<T> writer = new Neo4jItemWriter<>();
|
||||
writer.setDelete(this.delete);
|
||||
writer.setSessionFactory(this.sessionFactory);
|
||||
return writer;
|
||||
}
|
||||
/**
|
||||
* Validates and builds a {@link org.springframework.batch.extensions.neo4j.Neo4jItemWriter}.
|
||||
*
|
||||
* @return a {@link Neo4jItemWriter}
|
||||
*/
|
||||
public Neo4jItemWriter<T> build() {
|
||||
Assert.notNull(neo4jTemplate, "neo4jTemplate is required.");
|
||||
Assert.notNull(neo4jDriver, "neo4jDriver is required.");
|
||||
Assert.notNull(neo4jMappingContext, "neo4jMappingContext is required.");
|
||||
Neo4jItemWriter<T> writer = new Neo4jItemWriter<>();
|
||||
writer.setDelete(this.delete);
|
||||
writer.setNeo4jTemplate(this.neo4jTemplate);
|
||||
writer.setNeo4jDriver(this.neo4jDriver);
|
||||
writer.setNeo4jMappingContext(this.neo4jMappingContext);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,187 +16,123 @@
|
||||
|
||||
package org.springframework.batch.extensions.neo4j;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
import org.neo4j.ogm.session.Session;
|
||||
import org.neo4j.ogm.session.SessionFactory;
|
||||
import org.neo4j.cypherdsl.core.Cypher;
|
||||
import org.neo4j.cypherdsl.core.Node;
|
||||
import org.neo4j.cypherdsl.core.Statement;
|
||||
import org.springframework.data.neo4j.core.Neo4jTemplate;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class Neo4jItemReaderTests {
|
||||
|
||||
@Rule
|
||||
public MockitoRule rule = MockitoJUnit.rule().silent();
|
||||
private Neo4jTemplate neo4jTemplate;
|
||||
|
||||
@Mock
|
||||
private Iterable<String> result;
|
||||
@Mock
|
||||
private SessionFactory sessionFactory;
|
||||
@Mock
|
||||
private Session session;
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
neo4jTemplate = mock(Neo4jTemplate.class);
|
||||
}
|
||||
|
||||
private Neo4jItemReader<String> buildSessionBasedReader() throws Exception {
|
||||
Neo4jItemReader<String> reader = new Neo4jItemReader<>();
|
||||
private Neo4jItemReader<String> buildSessionBasedReader() {
|
||||
Neo4jItemReader<String> reader = new Neo4jItemReader<>();
|
||||
|
||||
reader.setSessionFactory(this.sessionFactory);
|
||||
reader.setTargetType(String.class);
|
||||
reader.setStartStatement("n=node(*)");
|
||||
reader.setReturnStatement("*");
|
||||
reader.setOrderByStatement("n.age");
|
||||
reader.setPageSize(50);
|
||||
reader.afterPropertiesSet();
|
||||
reader.setNeo4jTemplate(this.neo4jTemplate);
|
||||
reader.setTargetType(String.class);
|
||||
Node n = Cypher.anyNode().named("n");
|
||||
reader.setStatement(Cypher.match(n).returning(n));
|
||||
reader.setPageSize(50);
|
||||
reader.afterPropertiesSet();
|
||||
|
||||
return reader;
|
||||
}
|
||||
return reader;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAfterPropertiesSet() throws Exception {
|
||||
@Test
|
||||
public void testAfterPropertiesSet() {
|
||||
|
||||
Neo4jItemReader<String> reader = new Neo4jItemReader<>();
|
||||
Neo4jItemReader<String> reader = new Neo4jItemReader<>();
|
||||
|
||||
try {
|
||||
reader.afterPropertiesSet();
|
||||
fail("SessionFactory was not set but exception was not thrown.");
|
||||
} catch (IllegalStateException iae) {
|
||||
assertEquals("A SessionFactory is required", iae.getMessage());
|
||||
} catch (Throwable t) {
|
||||
fail("Wrong exception was thrown:" + t);
|
||||
}
|
||||
try {
|
||||
reader.afterPropertiesSet();
|
||||
fail("SessionFactory was not set but exception was not thrown.");
|
||||
} catch (IllegalStateException iae) {
|
||||
assertEquals("A Neo4jTemplate is required", iae.getMessage());
|
||||
} catch (Throwable t) {
|
||||
fail("Wrong exception was thrown:" + t);
|
||||
}
|
||||
|
||||
reader.setSessionFactory(this.sessionFactory);
|
||||
reader.setNeo4jTemplate(this.neo4jTemplate);
|
||||
|
||||
try {
|
||||
reader.afterPropertiesSet();
|
||||
fail("Target Type was not set but exception was not thrown.");
|
||||
} catch (IllegalStateException iae) {
|
||||
assertEquals("The type to be returned is required", iae.getMessage());
|
||||
} catch (Throwable t) {
|
||||
fail("Wrong exception was thrown:" + t);
|
||||
}
|
||||
try {
|
||||
reader.afterPropertiesSet();
|
||||
fail("Target Type was not set but exception was not thrown.");
|
||||
} catch (IllegalStateException iae) {
|
||||
assertEquals("The type to be returned is required", iae.getMessage());
|
||||
} catch (Throwable t) {
|
||||
fail("Wrong exception was thrown:" + t);
|
||||
}
|
||||
|
||||
reader.setTargetType(String.class);
|
||||
reader.setTargetType(String.class);
|
||||
|
||||
try {
|
||||
reader.afterPropertiesSet();
|
||||
fail("START was not set but exception was not thrown.");
|
||||
} catch (IllegalStateException iae) {
|
||||
assertEquals("A START statement is required", iae.getMessage());
|
||||
} catch (Throwable t) {
|
||||
fail("Wrong exception was thrown:" + t);
|
||||
}
|
||||
reader.setStatement(Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode()));
|
||||
|
||||
reader.setStartStatement("n=node(*)");
|
||||
reader.afterPropertiesSet();
|
||||
|
||||
try {
|
||||
reader.afterPropertiesSet();
|
||||
fail("RETURN was not set but exception was not thrown.");
|
||||
} catch (IllegalStateException iae) {
|
||||
assertEquals("A RETURN statement is required", iae.getMessage());
|
||||
} catch (Throwable t) {
|
||||
fail("Wrong exception was thrown:" + t);
|
||||
}
|
||||
reader = new Neo4jItemReader<>();
|
||||
reader.setNeo4jTemplate(this.neo4jTemplate);
|
||||
reader.setTargetType(String.class);
|
||||
reader.setStatement(Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode()));
|
||||
|
||||
reader.setReturnStatement("n.name, n.phone");
|
||||
reader.afterPropertiesSet();
|
||||
}
|
||||
|
||||
try {
|
||||
reader.afterPropertiesSet();
|
||||
fail("ORDER BY was not set but exception was not thrown.");
|
||||
} catch (IllegalStateException iae) {
|
||||
assertEquals("A ORDER BY statement is required", iae.getMessage());
|
||||
} catch (Throwable t) {
|
||||
fail("Wrong exception was thrown:" + t);
|
||||
}
|
||||
@Test
|
||||
public void testNullResultsWithSession() {
|
||||
|
||||
reader.setOrderByStatement("n.age");
|
||||
Neo4jItemReader<String> itemReader = buildSessionBasedReader();
|
||||
|
||||
reader.afterPropertiesSet();
|
||||
ArgumentCaptor<Statement> query = ArgumentCaptor.forClass(Statement.class);
|
||||
|
||||
reader = new Neo4jItemReader<>();
|
||||
reader.setSessionFactory(this.sessionFactory);
|
||||
reader.setTargetType(String.class);
|
||||
reader.setStartStatement("n=node(*)");
|
||||
reader.setReturnStatement("n.name, n.phone");
|
||||
reader.setOrderByStatement("n.age");
|
||||
when(this.neo4jTemplate.findAll(query.capture(), isNull(), eq(String.class))).thenReturn(List.of());
|
||||
|
||||
reader.afterPropertiesSet();
|
||||
}
|
||||
assertFalse(itemReader.doPageRead().hasNext());
|
||||
Node node = Cypher.anyNode().named("n");
|
||||
assertEquals(Cypher.match(node).returning(node).skip(0).limit(50).build().getCypher(), query.getValue().getCypher());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testNullResultsWithSession() throws Exception {
|
||||
}
|
||||
|
||||
Neo4jItemReader<String> itemReader = buildSessionBasedReader();
|
||||
@Test
|
||||
public void testNoResultsWithSession() {
|
||||
Neo4jItemReader<String> itemReader = buildSessionBasedReader();
|
||||
ArgumentCaptor<Statement> query = ArgumentCaptor.forClass(Statement.class);
|
||||
|
||||
ArgumentCaptor<String> query = ArgumentCaptor.forClass(String.class);
|
||||
when(this.neo4jTemplate.findAll(query.capture(), any(), eq(String.class))).thenReturn(List.of());
|
||||
|
||||
when(this.sessionFactory.openSession()).thenReturn(this.session);
|
||||
when(this.session.query(eq(String.class), query.capture(), isNull())).thenReturn(null);
|
||||
assertFalse(itemReader.doPageRead().hasNext());
|
||||
Node node = Cypher.anyNode().named("n");
|
||||
assertEquals(Cypher.match(node).returning(node).skip(0).limit(50).build().getCypher(), query.getValue().getCypher());
|
||||
}
|
||||
|
||||
assertFalse(itemReader.doPageRead().hasNext());
|
||||
assertEquals("START n=node(*) RETURN * ORDER BY n.age SKIP 0 LIMIT 50", query.getValue());
|
||||
}
|
||||
@Test
|
||||
public void testResultsWithMatchAndWhereWithSession() {
|
||||
Neo4jItemReader<String> itemReader = buildSessionBasedReader();
|
||||
itemReader.afterPropertiesSet();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testNoResultsWithSession() throws Exception {
|
||||
Neo4jItemReader<String> itemReader = buildSessionBasedReader();
|
||||
ArgumentCaptor<String> query = ArgumentCaptor.forClass(String.class);
|
||||
when(this.neo4jTemplate.findAll(any(Statement.class), isNull(), eq(String.class))).thenReturn(Arrays.asList("foo", "bar", "baz"));
|
||||
|
||||
when(this.sessionFactory.openSession()).thenReturn(this.session);
|
||||
when(this.session.query(eq(String.class), query.capture(), isNull())).thenReturn(result);
|
||||
when(result.iterator()).thenReturn(Collections.emptyIterator());
|
||||
assertTrue(itemReader.doPageRead().hasNext());
|
||||
}
|
||||
|
||||
assertFalse(itemReader.doPageRead().hasNext());
|
||||
assertEquals("START n=node(*) RETURN * ORDER BY n.age SKIP 0 LIMIT 50", query.getValue());
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Test
|
||||
public void testResultsWithMatchAndWhereWithSession() throws Exception {
|
||||
Neo4jItemReader<String> itemReader = buildSessionBasedReader();
|
||||
itemReader.setMatchStatement("n -- m");
|
||||
itemReader.setWhereStatement("has(n.name)");
|
||||
itemReader.setReturnStatement("m");
|
||||
itemReader.afterPropertiesSet();
|
||||
|
||||
when(this.sessionFactory.openSession()).thenReturn(this.session);
|
||||
when(this.session.query(String.class, "START n=node(*) MATCH n -- m WHERE has(n.name) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", null)).thenReturn(result);
|
||||
when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
|
||||
|
||||
assertTrue(itemReader.doPageRead().hasNext());
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Test
|
||||
public void testResultsWithMatchAndWhereWithParametersWithSession() throws Exception {
|
||||
Neo4jItemReader<String> itemReader = buildSessionBasedReader();
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("foo", "bar");
|
||||
itemReader.setParameterValues(params);
|
||||
itemReader.setMatchStatement("n -- m");
|
||||
itemReader.setWhereStatement("has(n.name)");
|
||||
itemReader.setReturnStatement("m");
|
||||
itemReader.afterPropertiesSet();
|
||||
|
||||
when(this.sessionFactory.openSession()).thenReturn(this.session);
|
||||
when(this.session.query(String.class, "START n=node(*) MATCH n -- m WHERE has(n.name) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", params)).thenReturn(result);
|
||||
when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
|
||||
|
||||
assertTrue(itemReader.doPageRead().hasNext());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,134 +16,453 @@
|
||||
|
||||
package org.springframework.batch.extensions.neo4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.internal.verification.Times;
|
||||
import org.neo4j.cypherdsl.core.Cypher;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.ExecutableQuery;
|
||||
import org.neo4j.driver.QueryConfig;
|
||||
import org.neo4j.driver.Record;
|
||||
import org.springframework.batch.item.Chunk;
|
||||
import org.springframework.data.mapping.Association;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.model.BasicPersistentEntity;
|
||||
import org.springframework.data.neo4j.core.Neo4jTemplate;
|
||||
import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
|
||||
import org.springframework.data.neo4j.core.mapping.*;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
import org.neo4j.ogm.session.Session;
|
||||
import org.neo4j.ogm.session.SessionFactory;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collector;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
public class Neo4jItemWriterTests {
|
||||
|
||||
@Rule
|
||||
public MockitoRule rule = MockitoJUnit.rule().silent();
|
||||
private Neo4jItemWriter<MyEntity> writer;
|
||||
|
||||
private Neo4jItemWriter<String> writer;
|
||||
private Neo4jTemplate neo4jTemplate;
|
||||
private Driver neo4jDriver;
|
||||
private Neo4jMappingContext neo4jMappingContext;
|
||||
|
||||
@Mock
|
||||
private SessionFactory sessionFactory;
|
||||
@Mock
|
||||
private Session session;
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
neo4jTemplate = mock(Neo4jTemplate.class);
|
||||
neo4jDriver = mock(Driver.class);
|
||||
neo4jMappingContext = mock(Neo4jMappingContext.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAfterPropertiesSet() throws Exception{
|
||||
@Test
|
||||
public void testAfterPropertiesSet() {
|
||||
|
||||
writer = new Neo4jItemWriter<>();
|
||||
writer = new Neo4jItemWriter<>();
|
||||
|
||||
try {
|
||||
writer.afterPropertiesSet();
|
||||
fail("SessionFactory was not set but exception was not thrown.");
|
||||
} catch (IllegalStateException iae) {
|
||||
assertEquals("A SessionFactory is required", iae.getMessage());
|
||||
} catch (Throwable t) {
|
||||
fail("Wrong exception was thrown.");
|
||||
}
|
||||
try {
|
||||
writer.afterPropertiesSet();
|
||||
fail("Neo4jTemplate was not set but exception was not thrown.");
|
||||
} catch (IllegalStateException iae) {
|
||||
assertEquals("A Neo4jTemplate is required", iae.getMessage());
|
||||
} catch (Throwable t) {
|
||||
fail("Wrong exception was thrown.");
|
||||
}
|
||||
|
||||
writer.setSessionFactory(this.sessionFactory);
|
||||
writer.setNeo4jTemplate(this.neo4jTemplate);
|
||||
|
||||
writer.afterPropertiesSet();
|
||||
try {
|
||||
writer.afterPropertiesSet();
|
||||
fail("Neo4jMappingContext was not set but exception was not thrown.");
|
||||
} catch (IllegalStateException iae) {
|
||||
assertEquals("A Neo4jMappingContext is required", iae.getMessage());
|
||||
} catch (Throwable t) {
|
||||
fail("Wrong exception was thrown.");
|
||||
}
|
||||
|
||||
writer = new Neo4jItemWriter<>();
|
||||
writer.setNeo4jMappingContext(this.neo4jMappingContext);
|
||||
|
||||
writer.setSessionFactory(this.sessionFactory);
|
||||
try {
|
||||
writer.afterPropertiesSet();
|
||||
fail("Neo4jDriver was not set but exception was not thrown.");
|
||||
} catch (IllegalStateException iae) {
|
||||
assertEquals("A Neo4j driver is required", iae.getMessage());
|
||||
} catch (Throwable t) {
|
||||
fail("Wrong exception was thrown.");
|
||||
}
|
||||
|
||||
writer.afterPropertiesSet();
|
||||
}
|
||||
writer.setNeo4jDriver(this.neo4jDriver);
|
||||
|
||||
@Test
|
||||
public void testWriteNullSession() throws Exception {
|
||||
writer.afterPropertiesSet();
|
||||
}
|
||||
|
||||
writer = new Neo4jItemWriter<>();
|
||||
@Test
|
||||
public void testWriteNoItems() {
|
||||
writer = new Neo4jItemWriter<>();
|
||||
|
||||
writer.setSessionFactory(this.sessionFactory);
|
||||
writer.afterPropertiesSet();
|
||||
writer.setNeo4jTemplate(this.neo4jTemplate);
|
||||
writer.setNeo4jDriver(this.neo4jDriver);
|
||||
writer.setNeo4jMappingContext(this.neo4jMappingContext);
|
||||
writer.afterPropertiesSet();
|
||||
|
||||
writer.write(null);
|
||||
writer.write(Chunk.of());
|
||||
|
||||
verifyNoInteractions(this.session);
|
||||
}
|
||||
verifyNoInteractions(this.neo4jTemplate);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteNullWithSession() throws Exception {
|
||||
writer = new Neo4jItemWriter<>();
|
||||
@Test
|
||||
public void testWriteItems() {
|
||||
writer = new Neo4jItemWriter<>();
|
||||
|
||||
writer.setSessionFactory(this.sessionFactory);
|
||||
writer.afterPropertiesSet();
|
||||
writer.setNeo4jTemplate(this.neo4jTemplate);
|
||||
writer.setNeo4jDriver(this.neo4jDriver);
|
||||
writer.setNeo4jMappingContext(this.neo4jMappingContext);
|
||||
writer.afterPropertiesSet();
|
||||
|
||||
when(this.sessionFactory.openSession()).thenReturn(this.session);
|
||||
writer.write(null);
|
||||
writer.write(Chunk.of(new MyEntity("foo"), new MyEntity("bar")));
|
||||
|
||||
verifyNoInteractions(this.session);
|
||||
}
|
||||
verify(this.neo4jTemplate).saveAll(List.of(new MyEntity("foo"), new MyEntity("bar")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteNoItemsWithSession() throws Exception {
|
||||
writer = new Neo4jItemWriter<>();
|
||||
@Test
|
||||
public void testDeleteItems() {
|
||||
TypeInformation<MyEntity> typeInformation = TypeInformation.of(MyEntity.class);
|
||||
NodeDescription<MyEntity> entity = new TestEntity<>(typeInformation);
|
||||
when(neo4jMappingContext.getNodeDescription(MyEntity.class)).thenAnswer(invocationOnMock -> entity);
|
||||
when(neo4jDriver.executableQuery(anyString())).thenReturn(new ExecutableQuery() {
|
||||
@Override
|
||||
public ExecutableQuery withParameters(Map<String, Object> parameters) {
|
||||
return this;
|
||||
}
|
||||
|
||||
writer.setSessionFactory(this.sessionFactory);
|
||||
writer.afterPropertiesSet();
|
||||
@Override
|
||||
public ExecutableQuery withConfig(QueryConfig config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
when(this.sessionFactory.openSession()).thenReturn(this.session);
|
||||
writer.write(new ArrayList<>());
|
||||
@Override
|
||||
public <A, R, T> T execute(Collector<Record, A, R> recordCollector, ResultFinisher<R, T> resultFinisher) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
verifyNoInteractions(this.session);
|
||||
}
|
||||
writer = new Neo4jItemWriter<>();
|
||||
|
||||
@Test
|
||||
public void testWriteItemsWithSession() throws Exception {
|
||||
writer = new Neo4jItemWriter<>();
|
||||
writer.setNeo4jTemplate(this.neo4jTemplate);
|
||||
writer.setNeo4jDriver(this.neo4jDriver);
|
||||
writer.setNeo4jMappingContext(this.neo4jMappingContext);
|
||||
writer.afterPropertiesSet();
|
||||
|
||||
writer.setSessionFactory(this.sessionFactory);
|
||||
writer.afterPropertiesSet();
|
||||
writer.setDelete(true);
|
||||
|
||||
List<String> items = new ArrayList<>();
|
||||
items.add("foo");
|
||||
items.add("bar");
|
||||
Chunk<MyEntity> myEntities = Chunk.of(new MyEntity("id1"), new MyEntity("id2"));
|
||||
writer.write(myEntities);
|
||||
|
||||
when(this.sessionFactory.openSession()).thenReturn(this.session);
|
||||
writer.write(items);
|
||||
verify(this.neo4jDriver, new Times(2)).executableQuery("MATCH (MyEntity) WHERE MyEntity.idField = $id DETACH DELETE MyEntity");
|
||||
}
|
||||
|
||||
verify(this.session).save("foo");
|
||||
verify(this.session).save("bar");
|
||||
}
|
||||
private record MyEntity(String idField) {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteItemsWithSession() throws Exception {
|
||||
writer = new Neo4jItemWriter<>();
|
||||
private static class TestEntity<T> extends BasicPersistentEntity<T, Neo4jPersistentProperty>
|
||||
implements Neo4jPersistentEntity<T> {
|
||||
|
||||
writer.setSessionFactory(this.sessionFactory);
|
||||
writer.afterPropertiesSet();
|
||||
public TestEntity(TypeInformation<T> information) {
|
||||
super(information);
|
||||
addPersistentProperty(new Neo4jPersistentProperty() {
|
||||
@Override
|
||||
public Neo4jPersistentPropertyConverter<?> getOptionalConverter() {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> items = new ArrayList<>();
|
||||
items.add("foo");
|
||||
items.add("bar");
|
||||
@Override
|
||||
public boolean isEntityWithRelationshipProperties() {
|
||||
return false;
|
||||
}
|
||||
|
||||
writer.setDelete(true);
|
||||
@Override
|
||||
public PersistentEntity<?, Neo4jPersistentProperty> getOwner() {
|
||||
return null;
|
||||
}
|
||||
|
||||
when(this.sessionFactory.openSession()).thenReturn(this.session);
|
||||
writer.write(items);
|
||||
@Override
|
||||
public String getName() {
|
||||
return "idField";
|
||||
}
|
||||
|
||||
verify(this.session).delete("foo");
|
||||
verify(this.session).delete("bar");
|
||||
}
|
||||
@Override
|
||||
public Class<?> getType() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeInformation<?> getTypeInformation() {
|
||||
return TypeInformation.of(String.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<? extends TypeInformation<?>> getPersistentEntityTypeInformation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Method getGetter() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Method getSetter() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Method getWither() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Field getField() {
|
||||
try {
|
||||
return MyEntity.class.getDeclaredField("idField");
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSpelExpression() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Association<Neo4jPersistentProperty> getAssociation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEntity() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIdProperty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVersionProperty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollectionLike() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMap() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isArray() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTransient() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWritable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isImmutable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAssociation() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getComponentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getRawType() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getMapValueType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getActualType() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Annotation> A findAnnotation(Class<A> annotationType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Annotation> A findPropertyOrOwnerAnnotation(Class<A> annotationType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean usePropertyAccess() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getAssociationTargetType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeInformation<?> getAssociationTargetTypeInformation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFieldName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPropertyName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInternalIdProperty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRelationship() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isComposite() {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Neo4jPersistentProperty> getDynamicLabelsProperty() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRelationshipPropertiesEntity() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrimaryLabel() {
|
||||
return "MyEntity";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMostAbstractParentLabel(NodeDescription<?> mostAbstractNodeDescription) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAdditionalLabels() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<T> getUnderlyingClass() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdDescription getIdDescription() {
|
||||
return IdDescription.forAssignedIds(Cypher.name("thing"), "idField");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<GraphPropertyDescription> getGraphProperties() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<GraphPropertyDescription> getGraphPropertiesInHierarchy() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<GraphPropertyDescription> getGraphProperty(String fieldName) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<RelationshipDescription> getRelationships() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<RelationshipDescription> getRelationshipsInHierarchy(Predicate<PropertyFilter.RelaxedPropertyPath> propertyPredicate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChildNodeDescription(NodeDescription<?> child) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<NodeDescription<?>> getChildNodeDescriptionsInHierarchy() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setParentNodeDescription(NodeDescription<?> parent) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeDescription<?> getParentNodeDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsPossibleCircles(Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean describesInterface() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,275 +16,176 @@
|
||||
|
||||
package org.springframework.batch.extensions.neo4j.builder;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
import org.neo4j.ogm.session.Session;
|
||||
import org.neo4j.ogm.session.SessionFactory;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.neo4j.cypherdsl.core.Cypher;
|
||||
import org.neo4j.cypherdsl.core.Statement;
|
||||
import org.neo4j.cypherdsl.core.StatementBuilder;
|
||||
import org.springframework.batch.extensions.neo4j.Neo4jItemReader;
|
||||
import org.springframework.data.neo4j.core.Neo4jTemplate;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.fail;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* @author Glenn Renfro
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
public class Neo4jItemReaderBuilderTests {
|
||||
|
||||
@Rule
|
||||
public MockitoRule rule = MockitoJUnit.rule().silent();
|
||||
private List<String> result;
|
||||
private Neo4jTemplate neo4jTemplate;
|
||||
private StatementBuilder.OngoingReadingAndReturn dummyStatement = Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode());
|
||||
|
||||
@Mock
|
||||
private Iterable<String> result;
|
||||
@SuppressWarnings("unchecked")
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
result = mock(List.class);
|
||||
neo4jTemplate = mock(Neo4jTemplate.class);
|
||||
}
|
||||
|
||||
@Mock
|
||||
private SessionFactory sessionFactory;
|
||||
@Test
|
||||
public void testFullyQualifiedItemReader() throws Exception {
|
||||
dummyStatement = Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode());
|
||||
Neo4jItemReader<String> itemReader = new Neo4jItemReaderBuilder<String>()
|
||||
.neo4jTemplate(this.neo4jTemplate)
|
||||
.targetType(String.class)
|
||||
.statement(dummyStatement)
|
||||
.pageSize(50).name("bar")
|
||||
.build();
|
||||
|
||||
@Mock
|
||||
private Session session;
|
||||
when(this.neo4jTemplate.findAll(any(Statement.class), any(), eq(String.class)))
|
||||
.thenReturn(result);
|
||||
when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
|
||||
|
||||
@Test
|
||||
public void testFullyQualifiedItemReader() throws Exception {
|
||||
Neo4jItemReader<String> itemReader = new Neo4jItemReaderBuilder<String>()
|
||||
.sessionFactory(this.sessionFactory)
|
||||
.targetType(String.class)
|
||||
.startStatement("n=node(*)")
|
||||
.orderByStatement("n.age")
|
||||
.pageSize(50).name("bar")
|
||||
.matchStatement("n -- m")
|
||||
.whereStatement("has(n.name)")
|
||||
.returnStatement("m").build();
|
||||
assertEquals("foo", itemReader.read());
|
||||
assertEquals("bar", itemReader.read());
|
||||
assertEquals("baz", itemReader.read());
|
||||
}
|
||||
|
||||
when(this.sessionFactory.openSession()).thenReturn(this.session);
|
||||
when(this.session.query(String.class,
|
||||
"START n=node(*) MATCH n -- m WHERE has(n.name) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", null))
|
||||
.thenReturn(result);
|
||||
when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
|
||||
@Test
|
||||
public void testCurrentSize() throws Exception {
|
||||
Neo4jItemReader<String> itemReader = new Neo4jItemReaderBuilder<String>()
|
||||
.neo4jTemplate(this.neo4jTemplate)
|
||||
.targetType(String.class)
|
||||
.statement(dummyStatement)
|
||||
.pageSize(50).name("bar")
|
||||
.currentItemCount(0)
|
||||
.maxItemCount(1)
|
||||
.build();
|
||||
|
||||
assertEquals("The expected value was not returned by reader.", "foo", itemReader.read());
|
||||
assertEquals("The expected value was not returned by reader.", "bar", itemReader.read());
|
||||
assertEquals("The expected value was not returned by reader.", "baz", itemReader.read());
|
||||
}
|
||||
when(this.neo4jTemplate.findAll(any(Statement.class), any(), eq(String.class)))
|
||||
.thenReturn(result);
|
||||
when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
|
||||
|
||||
@Test
|
||||
public void testCurrentSize() throws Exception {
|
||||
Neo4jItemReader<String> itemReader = new Neo4jItemReaderBuilder<String>()
|
||||
.sessionFactory(this.sessionFactory)
|
||||
.targetType(String.class)
|
||||
.startStatement("n=node(*)")
|
||||
.orderByStatement("n.age")
|
||||
.pageSize(50).name("bar")
|
||||
.returnStatement("m")
|
||||
.currentItemCount(0)
|
||||
.maxItemCount(1)
|
||||
.build();
|
||||
assertEquals("foo", itemReader.read());
|
||||
assertNull(itemReader.read());
|
||||
}
|
||||
|
||||
when(this.sessionFactory.openSession()).thenReturn(this.session);
|
||||
when(this.session.query(String.class, "START n=node(*) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", null))
|
||||
.thenReturn(result);
|
||||
when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
|
||||
|
||||
assertEquals("The expected value was not returned by reader.", "foo", itemReader.read());
|
||||
assertNull("The expected value was not should be null.", itemReader.read());
|
||||
}
|
||||
@Test
|
||||
public void testNoSessionFactory() {
|
||||
try {
|
||||
new Neo4jItemReaderBuilder<String>()
|
||||
.targetType(String.class)
|
||||
.pageSize(50)
|
||||
.name("bar").build();
|
||||
|
||||
@Test
|
||||
public void testResultsWithMatchAndWhereWithParametersWithSession() throws Exception {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("foo", "bar");
|
||||
Neo4jItemReader<String> itemReader = new Neo4jItemReaderBuilder<String>()
|
||||
.sessionFactory(this.sessionFactory)
|
||||
.targetType(String.class)
|
||||
.startStatement("n=node(*)")
|
||||
.returnStatement("*")
|
||||
.orderByStatement("n.age")
|
||||
.pageSize(50)
|
||||
.name("foo")
|
||||
.parameterValues(params)
|
||||
.matchStatement("n -- m")
|
||||
.whereStatement("has(n.name)")
|
||||
.returnStatement("m")
|
||||
.build();
|
||||
fail("IllegalArgumentException should have been thrown");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertEquals("neo4jTemplate is required.", iae.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
when(this.sessionFactory.openSession()).thenReturn(this.session);
|
||||
when(this.session.query(String.class,
|
||||
"START n=node(*) MATCH n -- m WHERE has(n.name) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", params))
|
||||
.thenReturn(result);
|
||||
when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
|
||||
@Test
|
||||
public void testZeroPageSize() {
|
||||
validateExceptionMessage(new Neo4jItemReaderBuilder<String>()
|
||||
.neo4jTemplate(this.neo4jTemplate)
|
||||
.targetType(String.class)
|
||||
.statement(dummyStatement)
|
||||
.pageSize(0)
|
||||
.name("foo"),
|
||||
"pageSize must be greater than zero");
|
||||
}
|
||||
|
||||
assertEquals("The expected value was not returned by reader.", "foo", itemReader.read());
|
||||
}
|
||||
@Test
|
||||
public void testZeroMaxItemCount() {
|
||||
validateExceptionMessage(new Neo4jItemReaderBuilder<String>()
|
||||
.neo4jTemplate(this.neo4jTemplate)
|
||||
.targetType(String.class)
|
||||
.statement(dummyStatement)
|
||||
.pageSize(5)
|
||||
.maxItemCount(0)
|
||||
.name("foo"),
|
||||
"maxItemCount must be greater than zero");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoSessionFactory() {
|
||||
try {
|
||||
new Neo4jItemReaderBuilder<String>()
|
||||
.targetType(String.class)
|
||||
.startStatement("n=node(*)")
|
||||
.returnStatement("*")
|
||||
.orderByStatement("n.age")
|
||||
.pageSize(50)
|
||||
.name("bar").build();
|
||||
@Test
|
||||
public void testCurrentItemCountGreaterThanMaxItemCount() {
|
||||
validateExceptionMessage(new Neo4jItemReaderBuilder<String>()
|
||||
.neo4jTemplate(this.neo4jTemplate)
|
||||
.targetType(String.class)
|
||||
.statement(dummyStatement)
|
||||
.pageSize(5)
|
||||
.maxItemCount(5)
|
||||
.currentItemCount(6)
|
||||
.name("foo"),
|
||||
"maxItemCount must be greater than currentItemCount");
|
||||
}
|
||||
|
||||
fail("IllegalArgumentException should have been thrown");
|
||||
}
|
||||
catch (IllegalArgumentException iae) {
|
||||
assertEquals("IllegalArgumentException message did not match the expected result.",
|
||||
"sessionFactory is required.", iae.getMessage());
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void testNullName() {
|
||||
validateExceptionMessage(
|
||||
new Neo4jItemReaderBuilder<String>()
|
||||
.neo4jTemplate(this.neo4jTemplate)
|
||||
.targetType(String.class)
|
||||
.statement(dummyStatement)
|
||||
.pageSize(50),
|
||||
"A name is required when saveState is set to true");
|
||||
|
||||
@Test
|
||||
public void testZeroPageSize() {
|
||||
validateExceptionMessage(new Neo4jItemReaderBuilder<String>()
|
||||
.sessionFactory(this.sessionFactory)
|
||||
.targetType(String.class)
|
||||
.startStatement("n=node(*)")
|
||||
.returnStatement("*")
|
||||
.orderByStatement("n.age")
|
||||
.pageSize(0)
|
||||
.name("foo")
|
||||
.matchStatement("n -- m")
|
||||
.whereStatement("has(n.name)")
|
||||
.returnStatement("m"),
|
||||
"pageSize must be greater than zero");
|
||||
}
|
||||
// tests that name is not required if saveState is set to false.
|
||||
new Neo4jItemReaderBuilder<String>()
|
||||
.neo4jTemplate(this.neo4jTemplate)
|
||||
.targetType(String.class)
|
||||
.statement(dummyStatement)
|
||||
.saveState(false)
|
||||
.pageSize(50)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testZeroMaxItemCount() {
|
||||
validateExceptionMessage(new Neo4jItemReaderBuilder<String>()
|
||||
.sessionFactory(this.sessionFactory)
|
||||
.targetType(String.class)
|
||||
.startStatement("n=node(*)")
|
||||
.returnStatement("*")
|
||||
.orderByStatement("n.age")
|
||||
.pageSize(5)
|
||||
.maxItemCount(0)
|
||||
.name("foo")
|
||||
.matchStatement("n -- m")
|
||||
.whereStatement("has(n.name)")
|
||||
.returnStatement("m"),
|
||||
"maxItemCount must be greater than zero");
|
||||
}
|
||||
@Test
|
||||
public void testNullTargetType() {
|
||||
validateExceptionMessage(
|
||||
new Neo4jItemReaderBuilder<String>()
|
||||
.neo4jTemplate(this.neo4jTemplate)
|
||||
.statement(dummyStatement)
|
||||
.pageSize(50)
|
||||
.name("bar"),
|
||||
"targetType is required.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCurrentItemCountGreaterThanMaxItemCount() {
|
||||
validateExceptionMessage(new Neo4jItemReaderBuilder<String>()
|
||||
.sessionFactory(this.sessionFactory)
|
||||
.targetType(String.class)
|
||||
.startStatement("n=node(*)")
|
||||
.returnStatement("*")
|
||||
.orderByStatement("n.age")
|
||||
.pageSize(5)
|
||||
.maxItemCount(5)
|
||||
.currentItemCount(6)
|
||||
.name("foo")
|
||||
.matchStatement("n -- m")
|
||||
.whereStatement("has(n.name)")
|
||||
.returnStatement("m"),
|
||||
"maxItemCount must be greater than currentItemCount");
|
||||
}
|
||||
@Test
|
||||
public void testNullStatement() {
|
||||
validateExceptionMessage(
|
||||
new Neo4jItemReaderBuilder<String>()
|
||||
.neo4jTemplate(this.neo4jTemplate)
|
||||
.targetType(String.class)
|
||||
.pageSize(50).name("bar"),
|
||||
"statement is required.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullName() {
|
||||
validateExceptionMessage(
|
||||
new Neo4jItemReaderBuilder<String>()
|
||||
.sessionFactory(this.sessionFactory)
|
||||
.targetType(String.class)
|
||||
.startStatement("n=node(*)")
|
||||
.returnStatement("*")
|
||||
.orderByStatement("n.age")
|
||||
.pageSize(50),
|
||||
"A name is required when saveState is set to true");
|
||||
|
||||
// tests that name is not required if saveState is set to false.
|
||||
new Neo4jItemReaderBuilder<String>()
|
||||
.sessionFactory(this.sessionFactory)
|
||||
.targetType(String.class)
|
||||
.startStatement("n=node(*)")
|
||||
.returnStatement("*")
|
||||
.orderByStatement("n.age")
|
||||
.saveState(false)
|
||||
.pageSize(50)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullTargetType() {
|
||||
validateExceptionMessage(
|
||||
new Neo4jItemReaderBuilder<String>()
|
||||
.sessionFactory(this.sessionFactory)
|
||||
.startStatement("n=node(*)")
|
||||
.returnStatement("*")
|
||||
.orderByStatement("n.age")
|
||||
.pageSize(50)
|
||||
.name("bar")
|
||||
.matchStatement("n -- m")
|
||||
.whereStatement("has(n.name)")
|
||||
.returnStatement("m"),
|
||||
"targetType is required.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullStartStatement() {
|
||||
validateExceptionMessage(
|
||||
new Neo4jItemReaderBuilder<String>()
|
||||
.sessionFactory(this.sessionFactory)
|
||||
.targetType(String.class)
|
||||
.returnStatement("*")
|
||||
.orderByStatement("n.age")
|
||||
.pageSize(50).name("bar")
|
||||
.matchStatement("n -- m")
|
||||
.whereStatement("has(n.name)")
|
||||
.returnStatement("m"),
|
||||
"startStatement is required.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullReturnStatement() {
|
||||
validateExceptionMessage(new Neo4jItemReaderBuilder<String>()
|
||||
.sessionFactory(this.sessionFactory)
|
||||
.targetType(String.class)
|
||||
.startStatement("n=node(*)")
|
||||
.orderByStatement("n.age")
|
||||
.pageSize(50).name("bar")
|
||||
.matchStatement("n -- m")
|
||||
.whereStatement("has(n.name)"), "returnStatement is required.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullOrderByStatement() {
|
||||
validateExceptionMessage(
|
||||
new Neo4jItemReaderBuilder<String>()
|
||||
.sessionFactory(this.sessionFactory)
|
||||
.targetType(String.class)
|
||||
.startStatement("n=node(*)")
|
||||
.returnStatement("*")
|
||||
.pageSize(50)
|
||||
.name("bar")
|
||||
.matchStatement("n -- m")
|
||||
.whereStatement("has(n.name)")
|
||||
.returnStatement("m"),
|
||||
"orderByStatement is required.");
|
||||
}
|
||||
|
||||
private void validateExceptionMessage(Neo4jItemReaderBuilder<?> builder, String message) {
|
||||
try {
|
||||
builder.build();
|
||||
fail("IllegalArgumentException should have been thrown");
|
||||
}
|
||||
catch (IllegalArgumentException iae) {
|
||||
assertEquals("IllegalArgumentException message did not match the expected result.", message,
|
||||
iae.getMessage());
|
||||
}
|
||||
}
|
||||
private void validateExceptionMessage(Neo4jItemReaderBuilder<?> builder, String message) {
|
||||
try {
|
||||
builder.build();
|
||||
fail("IllegalArgumentException should have been thrown");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertEquals(message, iae.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,80 +16,117 @@
|
||||
|
||||
package org.springframework.batch.extensions.neo4j.builder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
import org.neo4j.ogm.session.Session;
|
||||
import org.neo4j.ogm.session.SessionFactory;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.neo4j.cypherdsl.core.Cypher;
|
||||
import org.neo4j.cypherdsl.core.Functions;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.ExecutableQuery;
|
||||
import org.springframework.batch.extensions.neo4j.Neo4jItemWriter;
|
||||
import org.springframework.batch.item.Chunk;
|
||||
import org.springframework.data.mapping.IdentifierAccessor;
|
||||
import org.springframework.data.neo4j.core.Neo4jTemplate;
|
||||
import org.springframework.data.neo4j.core.mapping.IdDescription;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* @author Glenn Renfro
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
public class Neo4jItemWriterBuilderTests {
|
||||
|
||||
@Rule
|
||||
public MockitoRule rule = MockitoJUnit.rule().silent();
|
||||
private Neo4jTemplate neo4jTemplate;
|
||||
|
||||
@Mock
|
||||
private SessionFactory sessionFactory;
|
||||
@Mock
|
||||
private Session session;
|
||||
private Driver neo4jDriver;
|
||||
|
||||
@Test
|
||||
public void testBasicWriter() throws Exception{
|
||||
Neo4jItemWriter<String> writer = new Neo4jItemWriterBuilder<String>()
|
||||
.sessionFactory(this.sessionFactory)
|
||||
.build();
|
||||
List<String> items = new ArrayList<>();
|
||||
items.add("foo");
|
||||
items.add("bar");
|
||||
private Neo4jMappingContext neo4jMappingContext;
|
||||
|
||||
when(this.sessionFactory.openSession()).thenReturn(this.session);
|
||||
writer.write(items);
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
neo4jDriver = mock(Driver.class);
|
||||
neo4jTemplate = mock(Neo4jTemplate.class);
|
||||
neo4jMappingContext = mock(Neo4jMappingContext.class);
|
||||
}
|
||||
|
||||
verify(this.session).save("foo");
|
||||
verify(this.session).save("bar");
|
||||
verify(this.session, never()).delete("foo");
|
||||
verify(this.session, never()).delete("bar");
|
||||
}
|
||||
@Test
|
||||
public void testBasicWriter() {
|
||||
Neo4jItemWriter<String> writer = new Neo4jItemWriterBuilder<String>()
|
||||
.neo4jTemplate(this.neo4jTemplate)
|
||||
.neo4jDriver(this.neo4jDriver)
|
||||
.neo4jMappingContext(this.neo4jMappingContext)
|
||||
.build();
|
||||
|
||||
@Test
|
||||
public void testBasicDelete() throws Exception{
|
||||
Neo4jItemWriter<String> writer = new Neo4jItemWriterBuilder<String>().delete(true).sessionFactory(this.sessionFactory).build();
|
||||
List<String> items = new ArrayList<>();
|
||||
items.add("foo");
|
||||
items.add("bar");
|
||||
Chunk<String> items = Chunk.of("foo", "bar");
|
||||
writer.write(items);
|
||||
|
||||
when(this.sessionFactory.openSession()).thenReturn(this.session);
|
||||
writer.write(items);
|
||||
verify(this.neo4jTemplate).saveAll(items.getItems());
|
||||
verify(this.neo4jDriver, never()).executableQuery(anyString());
|
||||
}
|
||||
|
||||
verify(this.session).delete("foo");
|
||||
verify(this.session).delete("bar");
|
||||
verify(this.session, never()).save("foo");
|
||||
verify(this.session, never()).save("bar");
|
||||
}
|
||||
@Test
|
||||
public void testBasicDelete() {
|
||||
Neo4jItemWriter<String> writer = new Neo4jItemWriterBuilder<String>()
|
||||
.delete(true)
|
||||
.neo4jMappingContext(this.neo4jMappingContext)
|
||||
.neo4jTemplate(this.neo4jTemplate)
|
||||
.neo4jDriver(neo4jDriver)
|
||||
.build();
|
||||
|
||||
@Test
|
||||
public void testNoSessionFactory() {
|
||||
try {
|
||||
new Neo4jItemWriterBuilder<String>().build();
|
||||
fail("SessionFactory was not set but exception was not thrown.");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertEquals("sessionFactory is required.", iae.getMessage());
|
||||
}
|
||||
}
|
||||
// needs some mocks to create the testable environment
|
||||
Neo4jPersistentEntity<?> persistentEntity = mock(Neo4jPersistentEntity.class);
|
||||
IdentifierAccessor identifierAccessor = mock(IdentifierAccessor.class);
|
||||
IdDescription idDescription = mock(IdDescription.class);
|
||||
ExecutableQuery executableQuery = mock(ExecutableQuery.class);
|
||||
when(identifierAccessor.getRequiredIdentifier()).thenReturn("someId");
|
||||
when(idDescription.asIdExpression(anyString())).thenReturn(Functions.id(Cypher.anyNode()));
|
||||
when(executableQuery.withParameters(any())).thenReturn(executableQuery);
|
||||
when(persistentEntity.getIdentifierAccessor(any())).thenReturn(identifierAccessor);
|
||||
when(persistentEntity.getPrimaryLabel()).thenReturn("SomeLabel");
|
||||
when(persistentEntity.getIdDescription()).thenReturn(idDescription);
|
||||
when(this.neo4jMappingContext.getNodeDescription(any(Class.class))).thenAnswer(invocationOnMock -> persistentEntity);
|
||||
when(this.neo4jDriver.executableQuery(anyString())).thenReturn(executableQuery);
|
||||
|
||||
Chunk<String> items = Chunk.of("foo", "bar");
|
||||
|
||||
writer.write(items);
|
||||
|
||||
verify(this.neo4jDriver, times(2)).executableQuery(anyString());
|
||||
verify(this.neo4jTemplate, never()).save(items);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoNeo4jDriver() {
|
||||
try {
|
||||
new Neo4jItemWriterBuilder<String>().neo4jTemplate(neo4jTemplate).neo4jMappingContext(neo4jMappingContext).build();
|
||||
fail("Neo4jTemplate was not set but exception was not thrown.");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertEquals("neo4jDriver is required.", iae.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoMappingContextFactory() {
|
||||
try {
|
||||
new Neo4jItemWriterBuilder<String>().neo4jTemplate(neo4jTemplate).neo4jDriver(neo4jDriver).build();
|
||||
fail("Neo4jTemplate was not set but exception was not thrown.");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertEquals("neo4jMappingContext is required.", iae.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoNeo4jTemplate() {
|
||||
try {
|
||||
new Neo4jItemWriterBuilder<String>().build();
|
||||
fail("Neo4jTemplate was not set but exception was not thrown.");
|
||||
} catch (IllegalArgumentException iae) {
|
||||
assertEquals("neo4jTemplate is required.", iae.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user