Drop the notion of the NodeManager.
This removes the NodeManager and related infrastructure and introduces some configuration support. See more about the reasoning in ADR-004. Co-authored-by: Gerrit Meier <meistermeier@gmail.com>
This commit is contained in:
48
README.adoc
48
README.adoc
@@ -49,14 +49,7 @@ Each property of a class that is not identified as a simple type by `org.springf
|
||||
|
||||
NOTE: Context in this sections refers especially to dirty tracking and dealing with state of entities.
|
||||
|
||||
It is unlikely that we can do completely without context.
|
||||
While it's easy enough to update on entity as is with the <<schema>>, it's unlikely that we can deal this way with collections of relationships.
|
||||
Spring Data JDBC's approach is as follow
|
||||
____
|
||||
If the aggregate root is not new, all referenced entities get deleted, the aggregate root gets updated, and all referenced entities get inserted again.
|
||||
____
|
||||
|
||||
That sounds good in theory, but the Graph itself being more optimized for reads than ongoing large chunks of writes probably won't like that.
|
||||
We decided against a context for tracking changes, much like Spring Data JDBC did.
|
||||
|
||||
=== Other principles
|
||||
|
||||
@@ -182,7 +175,8 @@ It is only meant to be a basic for discussions.
|
||||
note "Implementation of Spring Data Commons SPI" as SDC_note
|
||||
package "org.springframework.data.neo4j" {
|
||||
package "core" {
|
||||
interface Neo4jTemplate
|
||||
interface Neo4jClient
|
||||
interface ReactiveNeo4jClient
|
||||
package "schema" {
|
||||
package "internal" {
|
||||
note "Schema description" as schemaDescription
|
||||
@@ -194,8 +188,8 @@ package "core" {
|
||||
interface Neo4jPersistentEntity
|
||||
interface Neo4jPersistentProperty
|
||||
}
|
||||
package "session" {
|
||||
interface Neo4jSessionFactory
|
||||
package "transaction" {
|
||||
class Neo4jTransactionManager
|
||||
}
|
||||
package "convert" {
|
||||
note "conversion support" as conversionNote
|
||||
@@ -234,7 +228,6 @@ core-[hidden]--->repository
|
||||
|`Neo4jTemplate` and related classes.
|
||||
|core.schema
|
||||
|Annotations for marking classes as nodes to be saved as well as internal schema description.
|
||||
|context
|
||||
|Infrastructure for dirty tracking etc.
|
||||
|core.mapping
|
||||
|Spring mapping information.
|
||||
@@ -291,10 +284,10 @@ The other execution paths are only drafts right now and marked with a `*`.
|
||||
From our previous experience and handling in other Spring Data stores this would branch off in two (technical three) directions:
|
||||
|
||||
. `StringBasedNeo4jQuery` for custom Cypher queries that are provided with the `@Query` annotation.
|
||||
. `*` `StringBasedNeo4jQuery` for named queries that are outsourced in property files.
|
||||
. `*` `PartTreeNeo4jQuery` for derived finder methods.
|
||||
. `StringBasedNeo4jQuery` for named queries that are outsourced in property files.
|
||||
. `PartTreeNeo4jQuery` for derived finder methods.
|
||||
|
||||
All three of them will get a custom `Neo4jQueryMethod` besides `Neo4jOperations` and `QueryMethodEvaluationContextProvider` (not used yet) provided.
|
||||
All three of them will get a custom `Neo4jQueryMethod` besides `Neo4jClient` and `QueryMethodEvaluationContextProvider` (not used yet) provided.
|
||||
This is a wrapper around the `java.lang.reflect.Method` passed into the `resolveQuery` method of the `Neo4jQueryLookupStrategy` to provide additional metadata.
|
||||
|
||||
==== `StringBasedNeo4jQuery` execution
|
||||
@@ -314,30 +307,7 @@ We considered several approaches of dirty tracking in SDN-RX:
|
||||
. Shallow copy of objects to get compared on save.
|
||||
_A full copy of the objects will occupy twice the memory._
|
||||
|
||||
Picking the third option for now (_using some kind of event / listener_) would allow us to track single field / relationship changes per object.
|
||||
An entity will get tracked after it gets initially saved into or loaded from the database.
|
||||
|
||||
[NOTE]
|
||||
.Architecture evaluation fragment (not current anymore but should get archived)
|
||||
====
|
||||
As the technical solution we decided to use proxies to listen for the changes.
|
||||
It will then be required for the user to work with the returned (proxy) object after calling `save` to set additional fields etc.
|
||||
The event tracking and event triggering through the proxies should communicate via an API to have the option of using another tracking approach in the future.
|
||||
|
||||
Moved back to evaluation: It is not possible to simply intercept arbitrary property changes in Java.
|
||||
We would need some kind of byte code enhancement/maven plugin to achieve this.
|
||||
====
|
||||
|
||||
The information about object changes will be based on an object comparison.
|
||||
We will store the saved or loaded object state and compare it with the actual version that should get saved.
|
||||
To keep the eventing idea up this will create an one-shot event containing multiple changed properties.
|
||||
This will also be needed if we implement the support for property changes through the byte code enhancement,
|
||||
because a change / domain interaction could affect more than one property at a time.
|
||||
The events will then get used to construct the matching cypher for the next save interaction.
|
||||
|
||||
==== Side-effect on "flush mode"
|
||||
We can also track repository interactions like `save` and defer them depending on the flush mode.
|
||||
This would make it possible to remove the need for calling `save` explicitly but be more JPA-ish.
|
||||
We have settled with option 1 (See ADR-004), analogue to Spring Data JDBC.
|
||||
|
||||
[[starter]]
|
||||
== Spring Boot Starter
|
||||
|
||||
22
etc/adr/adr-004.adoc
Normal file
22
etc/adr/adr-004.adoc
Normal file
@@ -0,0 +1,22 @@
|
||||
== ADR 4: Drop the notion of the `NodeManager`
|
||||
|
||||
=== Status
|
||||
|
||||
accepted
|
||||
|
||||
=== Context
|
||||
|
||||
We introduced the `NodeManager` as a pendan to Hibernates `EntityManager` and with it, a concept of a persistence context, tracking changes.
|
||||
This setup is required for updating only changed properties and also having implicit saves.
|
||||
|
||||
=== Decision
|
||||
|
||||
The previous versions of SDN and OGM all copied the concept of having a tracking of entities.
|
||||
We decided against it this time to remove complexity.
|
||||
We will update all properties each time a node is save, relying on the database to do this in an efficient way.
|
||||
|
||||
Relationships will be updated via smart queries.
|
||||
|
||||
=== Consequences
|
||||
|
||||
The biggest impact will probably more network traffic with models having a huge number of properties on a single domain object.
|
||||
@@ -16,22 +16,34 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core;
|
||||
package org.springframework.data.neo4j.config;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.data.neo4j.core.PersistenceException.IllegalResultSizeException;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
|
||||
/**
|
||||
* Throw when a query doesn't return a unique result.
|
||||
* Base class for imperative SDN-RX configuration using JavaConfig.
|
||||
* This can be included in all scenarios in which Spring Boot is not an option.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Deichkind - Niveau weshalb warum
|
||||
* @author Gerrit Meier
|
||||
* @since 1.0
|
||||
*/
|
||||
@Configuration
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public class NonUniqueResultException extends IllegalResultSizeException {
|
||||
public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport {
|
||||
|
||||
public NonUniqueResultException(String query, long actualNumberOfResults) {
|
||||
super(1L, actualNumberOfResults, query);
|
||||
/**
|
||||
* The driver used here should be the driver resulting from {@link #driver()}, which is the default.
|
||||
*
|
||||
* @param driver The driver to connect with.
|
||||
* @return A imperative Neo4j client.
|
||||
*/
|
||||
@Bean
|
||||
public Neo4jClient neo4jClient(Driver driver) {
|
||||
return Neo4jClient.create(driver);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.config;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
import org.springframework.data.neo4j.core.schema.Node;
|
||||
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Internal support class for basic configuration. The support infrastructure here is basically all around finding out about
|
||||
* which classes are to be mapped and which not. The driver is part of the configuration support, as Neo4js Java driver
|
||||
* contains both imperative and reactive components.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @author Gerrit Meier
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public abstract class Neo4jConfigurationSupport {
|
||||
|
||||
/**
|
||||
* The driver to be used for interacting with Neo4j.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public abstract Driver driver();
|
||||
|
||||
/**
|
||||
* Creates a {@link org.springframework.data.neo4j.core.mapping.Neo4jMappingContext} equipped with entity classes
|
||||
* scanned from the mapping base package.
|
||||
*
|
||||
* @return
|
||||
* @see #getMappingBasePackages()
|
||||
*/
|
||||
@Bean
|
||||
public Neo4jMappingContext neo4jMappingContext() throws ClassNotFoundException {
|
||||
|
||||
Neo4jMappingContext mappingContext = new Neo4jMappingContext();
|
||||
mappingContext.setInitialEntitySet(getInitialEntitySet());
|
||||
|
||||
return mappingContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a {@link PlatformTransactionManager} for Neo4j based on the driver resulting from {@link #driver()}.
|
||||
*
|
||||
* @param driver The driver to synchronize against
|
||||
* @return A platform transaction manager
|
||||
*/
|
||||
@Bean
|
||||
public PlatformTransactionManager transactionManager(Driver driver) {
|
||||
|
||||
return new Neo4jTransactionManager(driver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base packages to scan for Neo4j mapped entities at startup. Will return the package name of the
|
||||
* configuration class' (the concrete class, not this one here) by default. So if you have a
|
||||
* {@code com.acme.AppConfig} extending {@link Neo4jConfigurationSupport} the base package will be considered
|
||||
* {@code com.acme} unless the method is overridden to implement alternate behavior.
|
||||
*
|
||||
* @return the base packages to scan for mapped {@link Node} classes
|
||||
* or an empty collection to not enable scanning for entities.
|
||||
*/
|
||||
protected final Collection<String> getMappingBasePackages() {
|
||||
|
||||
Package mappingBasePackage = getClass().getPackage();
|
||||
return Collections.singleton(mappingBasePackage == null ? null : mappingBasePackage.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the mapping base package for classes annotated with {@link Node}.
|
||||
* By default, it scans for entities in all packages returned by {@link #getMappingBasePackages()}.
|
||||
*
|
||||
* @return
|
||||
* @throws ClassNotFoundException
|
||||
* @see #getMappingBasePackages()
|
||||
*/
|
||||
protected final Set<Class<?>> getInitialEntitySet() throws ClassNotFoundException {
|
||||
|
||||
Set<Class<?>> initialEntitySet = new HashSet<Class<?>>();
|
||||
|
||||
for (String basePackage : getMappingBasePackages()) {
|
||||
initialEntitySet.addAll(scanForEntities(basePackage));
|
||||
}
|
||||
|
||||
return initialEntitySet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the given base package for entities, i.e. Neo4j specific types annotated with {@link Node}.
|
||||
*
|
||||
* @param basePackage must not be {@literal null}.
|
||||
* @return
|
||||
* @throws ClassNotFoundException
|
||||
*/
|
||||
protected final Set<Class<?>> scanForEntities(String basePackage) throws ClassNotFoundException {
|
||||
|
||||
if (!StringUtils.hasText(basePackage)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
Set<Class<?>> initialEntitySet = new HashSet<Class<?>>();
|
||||
|
||||
ClassPathScanningCandidateComponentProvider componentProvider =
|
||||
new ClassPathScanningCandidateComponentProvider(false);
|
||||
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Node.class));
|
||||
|
||||
ClassLoader classLoader = Neo4jConfigurationSupport.class.getClassLoader();
|
||||
for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
|
||||
initialEntitySet.add(ClassUtils.forName(candidate.getBeanClassName(), classLoader));
|
||||
}
|
||||
|
||||
return initialEntitySet;
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core;
|
||||
|
||||
import static java.util.stream.Collectors.*;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Transaction;
|
||||
import org.neo4j.driver.exceptions.NoSuchRecordException;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient.MappingSpec;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient.RecordFetchSpec;
|
||||
import org.springframework.data.neo4j.core.context.DefaultPersistenceContext;
|
||||
import org.springframework.data.neo4j.core.context.PersistenceContext;
|
||||
import org.springframework.data.neo4j.core.schema.NodeDescription;
|
||||
import org.springframework.data.neo4j.core.schema.Schema;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @author Gerrit Meier
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
class DefaultNodeManager implements NodeManager {
|
||||
|
||||
private final Schema schema;
|
||||
|
||||
private final Neo4jClient neo4jClient;
|
||||
|
||||
private final Transaction transaction;
|
||||
|
||||
private final PersistenceContext persistenceContext;
|
||||
|
||||
DefaultNodeManager(Schema schema, Neo4jClient neo4jClient, @Nullable Transaction transaction) {
|
||||
|
||||
this.schema = schema;
|
||||
this.neo4jClient = neo4jClient;
|
||||
this.transaction = transaction;
|
||||
|
||||
this.persistenceContext = new DefaultPersistenceContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Transaction getTransaction() {
|
||||
return transaction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ExecutableQuery<T> toExecutableQuery(PreparedQuery<T> preparedQuery) {
|
||||
|
||||
Class<T> resultType = preparedQuery.getResultType();
|
||||
MappingSpec<Optional<T>, Collection<T>, T> mappingSpec = neo4jClient.newQuery(preparedQuery.getCypherQuery())
|
||||
.bindAll(preparedQuery.getParameters())
|
||||
.fetchAs(resultType);
|
||||
RecordFetchSpec<Optional<T>, Collection<T>, T> fetchSpec = preparedQuery.getOptionalMappingFunction()
|
||||
.map(mappingFunction -> mappingSpec.mappedBy(mappingFunction))
|
||||
.orElse(mappingSpec);
|
||||
|
||||
return new DefaultExecutableQuery(preparedQuery, schema.getNodeDescription(resultType), fetchSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T save(T entityWithUnknownState) {
|
||||
|
||||
// TODO if already registered, here or in the context?
|
||||
this.persistenceContext
|
||||
.register(entityWithUnknownState, schema.getRequiredNodeDescription(entityWithUnknownState.getClass()));
|
||||
|
||||
throw new UnsupportedOperationException("Not there yet.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Object managedEntity) {
|
||||
|
||||
this.persistenceContext.deregister(managedEntity);
|
||||
|
||||
throw new UnsupportedOperationException("Not there yet.");
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
class DefaultExecutableQuery<T> implements ExecutableQuery<T> {
|
||||
|
||||
private final PreparedQuery<T> preparedQuery;
|
||||
private final Optional<NodeDescription<?>> optionalNodeDescription;
|
||||
private final RecordFetchSpec<Optional<T>, Collection<T>, T> fetchSpec;
|
||||
|
||||
@Override
|
||||
public List<T> getResults() {
|
||||
return fetchSpec.all().stream().map(this::register).collect(toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<T> getSingleResult() {
|
||||
try {
|
||||
return fetchSpec.one().map(this::register);
|
||||
} catch (NoSuchRecordException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getRequiredSingleResult() {
|
||||
return fetchSpec.one().map(this::register)
|
||||
.orElseThrow(() -> new NoResultException(1L, preparedQuery.getCypherQuery()));
|
||||
}
|
||||
|
||||
private T register(T entity) {
|
||||
this.optionalNodeDescription.ifPresent(
|
||||
nodeDescription -> DefaultNodeManager.this.persistenceContext.register(entity, nodeDescription));
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Transaction;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Entry point for creating queries that return managed Nodes. The node manager is not supposed to be kept around
|
||||
* for longer than necessary. Try to keep your transactions short to avoid memory pressure due to keeping track of
|
||||
* managed nodes.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public interface NodeManager {
|
||||
|
||||
/**
|
||||
* Clears all managed entities and flushes any open state to the underlying storage.
|
||||
* TODO reflect if we really, really want to have this.
|
||||
* If an user flushes the node manager, we will end up in a state where we do not have any information about
|
||||
* the nodes / entities currently processed in the ongoing transaction.
|
||||
*/
|
||||
default void flush() {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Transaction getTransaction();
|
||||
|
||||
<T> ExecutableQuery<T> toExecutableQuery(PreparedQuery<T> preparedQuery);
|
||||
|
||||
/**
|
||||
* Saves an entity. When the entity is not yet managed in this instance of the NodeManager, and will be registered as
|
||||
* a managed instance. In either way, the state of the entity will be written to the underlying store afterwards.
|
||||
* It is recommended to use the returned, managed instance of the object. This is especially true when dealing with
|
||||
* immutable entities, where SDN RX has to return new instances to fill in generated keys and the like.
|
||||
*
|
||||
* @param entityWithUnknownState An entity that is either managed or unmanaged
|
||||
* @return A managed object
|
||||
*/
|
||||
<T> T save(T entityWithUnknownState);
|
||||
|
||||
/**
|
||||
* Delete an object from the persistence context and the underlying store.
|
||||
*
|
||||
* @param managedEntity Object to be removed
|
||||
*/
|
||||
void delete(Object managedEntity);
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.springframework.data.neo4j.core.schema.Schema;
|
||||
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Creates ready to use instances of {@link NodeManager}.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
@Slf4j
|
||||
public final class NodeManagerFactory {
|
||||
|
||||
private final AtomicBoolean initialized = new AtomicBoolean(false);
|
||||
|
||||
/**
|
||||
* Driver that is used to create new sessions, either by directly invoking it or through Springs transactional utils.
|
||||
*/
|
||||
private final Driver driver;
|
||||
|
||||
/**
|
||||
* The initial set of classes that will be registered with the schema {@link #initialize()} to build the schema for node managers
|
||||
* belonging to this factory.
|
||||
*/
|
||||
private final Set<Class<?>> initialPersistentClasses;
|
||||
|
||||
@Nullable
|
||||
private Schema schema;
|
||||
|
||||
/**
|
||||
* Creates a new instance of a factory producing {@link NodeManager node managers}. When used in a transactional setup,
|
||||
* i.e. with the {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager}, make sure to use
|
||||
* the same {@link Driver driver instance} for both the node and the transaction manager.
|
||||
* <p>
|
||||
* Spring Boots autoconfiguration for SDN RX will make sure that the same driver is used for both concerns.
|
||||
*
|
||||
* @param driver The driver used to obtain statement runners from when creating instances of node managers.
|
||||
* @param initialPersistentClasses The set of classes that should be initially scanned
|
||||
*/
|
||||
public NodeManagerFactory(Driver driver, Class<?>... initialPersistentClasses) {
|
||||
this(driver, Arrays.asList(initialPersistentClasses));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see NodeManagerFactory#NodeManagerFactory(Driver, Class...)
|
||||
*/
|
||||
public NodeManagerFactory(Driver driver, Collection<Class<?>> initialPersistentClasses) {
|
||||
this.driver = driver;
|
||||
this.initialPersistentClasses = new HashSet<>(initialPersistentClasses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new node manager. The returned manager is supposed to have a short lifetime. When used in a Spring setup,
|
||||
* this method should not called directly by client code. Instead the client code should use an injected instance of
|
||||
* {@link NodeManager}, which will participate in Springs application based transaction or in JTA transactions.
|
||||
*
|
||||
* @return A new node manager
|
||||
*/
|
||||
public NodeManager createNodeManager() {
|
||||
|
||||
if (!initialized.get()) {
|
||||
throw new IllegalStateException(
|
||||
"This factory has not been correctly initialized. Please provider a schema and register your persistent classes.");
|
||||
}
|
||||
// The call here to our Spring transaction shim has to be rethought in case we move this out of a Spring scope.
|
||||
// I dropped all the methods to configure that in an effort to make the setup more simple. ^mjs
|
||||
return new DefaultNodeManager(schema, Neo4jClient.create(driver),
|
||||
Neo4jTransactionUtils.retrieveTransaction(driver, null).orElse(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the schema for this node manager factory. The schemas has to be set before a node manager is retrieved from this factory.
|
||||
*
|
||||
* @param schema
|
||||
*/
|
||||
public void setSchema(@Nullable Schema schema) {
|
||||
this.schema = schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* This initializes this factory and is usually called by Springs infrastructure and only useful as standalone call
|
||||
* when the node manager factory is used without Spring.
|
||||
*/
|
||||
public void initialize() {
|
||||
|
||||
if (this.initialized.compareAndSet(false, true)) {
|
||||
Objects.requireNonNull(schema, "A schema is required. Did you provide one with #setSchema() beforehand?");
|
||||
log.info("Initializing schema with {} persistent classes", this.initialPersistentClasses.size());
|
||||
this.schema.register(this.initialPersistentClasses);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
|
||||
/**
|
||||
* Shared base class for exceptions during persistence operations of a {@link NodeManager}.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Deichkind - Niveau weshalb warum
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public abstract class PersistenceException extends RuntimeException {
|
||||
|
||||
protected PersistenceException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
protected PersistenceException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
abstract static class IllegalResultSizeException extends PersistenceException {
|
||||
|
||||
private final long expectedNumberOfResults;
|
||||
private final long actualNumberOfResults;
|
||||
private final String query;
|
||||
|
||||
IllegalResultSizeException(long expectedNumberOfResults, long actualNumberOfResults, String query) {
|
||||
|
||||
super(String.format("Expected %d results, got %d", expectedNumberOfResults, actualNumberOfResults));
|
||||
|
||||
this.query = query;
|
||||
this.expectedNumberOfResults = expectedNumberOfResults;
|
||||
this.actualNumberOfResults = actualNumberOfResults;
|
||||
}
|
||||
|
||||
public long getExpectedNumberOfResults() {
|
||||
return expectedNumberOfResults;
|
||||
}
|
||||
|
||||
public long getActualNumberOfResults() {
|
||||
return actualNumberOfResults;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core.context;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.neo4j.core.context.tracking.EntityChangeEvent;
|
||||
import org.springframework.data.neo4j.core.context.tracking.EntityComparisonStrategy;
|
||||
import org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy;
|
||||
import org.springframework.data.neo4j.core.schema.NodeDescription;
|
||||
|
||||
/**
|
||||
* TODO explain what the persistence context should do other than being the plural of EntityTrackingStrategy :D
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
@Slf4j
|
||||
public class DefaultPersistenceContext implements PersistenceContext {
|
||||
|
||||
private final EntityTrackingStrategy entityTrackingStrategy;
|
||||
|
||||
private final Set<Integer> registeredObjectIds = new HashSet<>();
|
||||
|
||||
public DefaultPersistenceContext() {
|
||||
this.entityTrackingStrategy = getEntityTrackingStrategy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Object entity, NodeDescription<?> description) {
|
||||
|
||||
Objects.requireNonNull(entity, "Cannot track null-entity!");
|
||||
|
||||
int identityOfEntity = getIdentityOf(entity);
|
||||
|
||||
if (registeredObjectIds.contains(identityOfEntity)) {
|
||||
log.debug("Object " + entity + " was already registered");
|
||||
return;
|
||||
}
|
||||
|
||||
registeredObjectIds.add(identityOfEntity);
|
||||
entityTrackingStrategy.track(entity, description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deregister(Object managedEntity) {
|
||||
|
||||
int identityOfEntity = getIdentityOf(managedEntity);
|
||||
if (!registeredObjectIds.contains(identityOfEntity)) {
|
||||
log.info("Cannot deregister " + managedEntity + " because it were never registered");
|
||||
return;
|
||||
}
|
||||
|
||||
entityTrackingStrategy.untrack(managedEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<EntityChanges> getEntityChanges(Object... objects) {
|
||||
|
||||
List<EntityChanges> entityChanges = new ArrayList<>();
|
||||
|
||||
for (Object object : objects) {
|
||||
Collection<EntityChangeEvent> changeEvents = entityTrackingStrategy.getAggregatedEntityChangeEvents(object);
|
||||
entityChanges.add(new EntityChanges(object, changeEvents));
|
||||
}
|
||||
|
||||
return entityChanges;
|
||||
}
|
||||
|
||||
EntityTrackingStrategy getEntityTrackingStrategy() {
|
||||
// todo choose the right / fitting implementation
|
||||
return new EntityComparisonStrategy();
|
||||
}
|
||||
|
||||
private int getIdentityOf(Object entity) {
|
||||
return entityTrackingStrategy.getObjectIdentifier(entity);
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class EntityChanges {
|
||||
private final Object entity;
|
||||
|
||||
private final Collection<EntityChangeEvent> changeEvents;
|
||||
|
||||
EntityChanges(Object entity, Collection<EntityChangeEvent> changeEvents) {
|
||||
this.entity = entity;
|
||||
this.changeEvents = changeEvents;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core.context;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.data.neo4j.core.schema.NodeDescription;
|
||||
|
||||
/**
|
||||
* Represents the state of varies instances being tracked. Those include:
|
||||
* <ul>
|
||||
* <li>All nodes</li>
|
||||
* <li>All relationships</li>
|
||||
* </ul>
|
||||
* including properties of them.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public interface PersistenceContext {
|
||||
|
||||
/**
|
||||
* Registers an entity within the context and starts tracking the entities state.
|
||||
*
|
||||
* @param entity The entity to register
|
||||
* @param description The schema description of the entities class
|
||||
*/
|
||||
void register(Object entity, NodeDescription<?> description);
|
||||
|
||||
/**
|
||||
* Removes an entity from this context and stops tracking the entitys state.
|
||||
*
|
||||
* @param managedEntity The entity to remove
|
||||
*/
|
||||
void deregister(Object managedEntity);
|
||||
|
||||
/**
|
||||
* Calculates the deltas for each registered object and returns them grouped by the object identifying function
|
||||
* defined in
|
||||
* {@link org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy#getObjectIdentifier(Object)} or
|
||||
* one of its implementations.
|
||||
*
|
||||
* @param objects to calculate and return changes for
|
||||
* @return All change events that got registered since registration.
|
||||
*/
|
||||
Collection<DefaultPersistenceContext.EntityChanges> getEntityChanges(Object... objects);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Contains all necessary infrastructure for the context of a session, i.e. dirty tracking and related.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@NonNullApi
|
||||
package org.springframework.data.neo4j.core.context;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core.context.tracking;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
@Getter
|
||||
public final class EntityChangeEvent {
|
||||
private final String propertyField;
|
||||
private final Object value;
|
||||
|
||||
EntityChangeEvent(String propertyField, Object value) {
|
||||
this.propertyField = propertyField;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core.context.tracking;
|
||||
|
||||
import static org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy.*;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.data.neo4j.core.schema.GraphPropertyDescription;
|
||||
import org.springframework.data.neo4j.core.schema.NodeDescription;
|
||||
|
||||
/**
|
||||
* Dirty tracking strategy based on property comparison. The strategy can compare two states of the same instance on
|
||||
* attribute level but uses hashes for collection-like fields. Only fields that are considered to be node properties
|
||||
* will get compared.
|
||||
*
|
||||
* @author Gerrit Meier
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
public class EntityComparisonStrategy implements EntityTrackingStrategy {
|
||||
|
||||
private final Map<Integer, EntityState> statesOfEntities = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void track(Object entity, NodeDescription nodeDescription) {
|
||||
statesOfEntities.put(getObjectIdentifier(entity),
|
||||
new EntityState(nodeDescription, entity, getDefaultObjectIdentifier()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void untrack(Object entity) {
|
||||
statesOfEntities.remove(getObjectIdentifier(entity));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<EntityChangeEvent> getAggregatedEntityChangeEvents(Object entity) {
|
||||
int objectIdentifier = getObjectIdentifier(entity);
|
||||
return statesOfEntities.get(objectIdentifier).aggregateChanges(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two entities of the same instance and creates a delta {@link EntityChangeEvent}.
|
||||
*/
|
||||
private static class EntityState {
|
||||
|
||||
private final Map<String, Object> oldState;
|
||||
private final List<Field> objectFields;
|
||||
private final Object identifier;
|
||||
private final Function<Object, Integer> objectIdentifyingFuction;
|
||||
|
||||
EntityState(NodeDescription nodeDescription, Object entity, Function<Object, Integer> objectIdentifyingFuction) {
|
||||
|
||||
Collection<GraphPropertyDescription> properties = nodeDescription.getGraphProperties();
|
||||
this.objectFields = getFieldsFromProperties(entity, properties);
|
||||
|
||||
this.objectIdentifyingFuction = objectIdentifyingFuction;
|
||||
this.identifier = this.objectIdentifyingFuction.apply(entity);
|
||||
this.oldState = retrieveStateFrom(entity);
|
||||
}
|
||||
|
||||
Set<EntityChangeEvent> aggregateChanges(Object newObject) {
|
||||
|
||||
if (!sameObject(objectIdentifyingFuction.apply(newObject))) {
|
||||
throw new IllegalArgumentException("The objects to compare are not the same.");
|
||||
}
|
||||
Set<EntityChangeEvent> changes = new HashSet<>();
|
||||
|
||||
for (Field field : objectFields) {
|
||||
Object newValue = getFieldValueOrHashCode(field, newObject);
|
||||
Object oldValue = oldState.get(field.getName());
|
||||
|
||||
// check oldValue null first to avoid NPE in `equals` calls
|
||||
if (oldValue == null && newValue == null) {
|
||||
continue;
|
||||
}
|
||||
if (oldValue == null || !oldValue.equals(newValue)) {
|
||||
changes.add(new EntityChangeEvent(field.getName(), newValue));
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private List<Field> getFieldsFromProperties(Object entity, Collection<GraphPropertyDescription> properties) {
|
||||
|
||||
List<Field> fields = new ArrayList<>();
|
||||
|
||||
Class<?> entityClass = entity.getClass();
|
||||
Map<String, Field> classFields = retrieveAllFields(entityClass);
|
||||
for (GraphPropertyDescription property : properties) {
|
||||
fields.add(classFields.get(property.getFieldName()));
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
private Map<String, Field> retrieveAllFields(Class<?> entityClass) {
|
||||
|
||||
Map<String, Field> classFields = new HashMap<>();
|
||||
|
||||
Class<?> classToScan = entityClass;
|
||||
|
||||
do {
|
||||
for (Field field : classToScan.getDeclaredFields()) {
|
||||
classFields.put(field.getName(), field);
|
||||
}
|
||||
classToScan = classToScan.getSuperclass();
|
||||
} while (classToScan != null);
|
||||
|
||||
return classFields;
|
||||
}
|
||||
|
||||
private boolean sameObject(Integer objectIdentifier) {
|
||||
return this.identifier.equals(objectIdentifier);
|
||||
}
|
||||
|
||||
private Map<String, Object> retrieveStateFrom(Object entity) {
|
||||
|
||||
Map<String, Object> state = new HashMap<>();
|
||||
|
||||
for (Field field : objectFields) {
|
||||
state.put(field.getName(), getFieldValueOrHashCode(field, entity));
|
||||
}
|
||||
|
||||
return Collections.unmodifiableMap(state);
|
||||
}
|
||||
|
||||
private Object getFieldValueOrHashCode(Field field, Object entity) {
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(entity);
|
||||
field.setAccessible(false);
|
||||
if (value == null) {
|
||||
return null;
|
||||
} else if (!Collection.class.isAssignableFrom(value.getClass())) {
|
||||
return value;
|
||||
} else {
|
||||
return value.hashCode();
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new IllegalStateException("Cannot determine field value", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core.context.tracking;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.data.neo4j.core.schema.NodeDescription;
|
||||
|
||||
/**
|
||||
* A tracking strategy is used to determine if an entity has changed it state over time in a transaction. It is also
|
||||
* responsible to generate and return a complete list of all these changes on an entity's attribute level.
|
||||
*
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public interface EntityTrackingStrategy {
|
||||
|
||||
static Function<Object, Integer> getDefaultObjectIdentifier() {
|
||||
return System::identityHashCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an entity in the entity tracking strategy. {@link NodeDescription} is needed to determine the fields that
|
||||
* are considered mapping relevant.
|
||||
*
|
||||
* @param entity the object that should get tracked.
|
||||
* @param nodeDescription the "rich" entity's class description.
|
||||
*/
|
||||
void track(Object entity, NodeDescription nodeDescription);
|
||||
|
||||
/**
|
||||
* Remove an entity from tracking. This method should get called after an entity was deleted to have a clean change
|
||||
* history state.
|
||||
*
|
||||
* @param entity to get removed from entity tracking
|
||||
*/
|
||||
void untrack(Object entity);
|
||||
|
||||
/**
|
||||
* Aggregates all changes that were registered for this entity since the start of tracking.
|
||||
*
|
||||
* @param entity for which a summarized collection of {@EntityChangeEvent}s should get created.
|
||||
* @return all events that occurred since tracking started.
|
||||
*/
|
||||
Collection<EntityChangeEvent> getAggregatedEntityChangeEvents(Object entity);
|
||||
|
||||
/**
|
||||
* Returns an object identifier for a given entity.
|
||||
*
|
||||
* @param entity the entity to get an unique identifier for.
|
||||
* @return hash or something similar that represents the entity.
|
||||
*/
|
||||
default int getObjectIdentifier(Object entity) {
|
||||
return getDefaultObjectIdentifier().apply(entity);
|
||||
}
|
||||
}
|
||||
@@ -138,13 +138,6 @@ public class Neo4jMappingContext
|
||||
return new DefaultNeo4jPersistentProperty(property, neo4jPersistentProperties, simpleTypeHolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void register(Set<? extends Class<?>> entityClasses) {
|
||||
|
||||
this.setInitialEntitySet(entityClasses);
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<NodeDescription<?>> getNodeDescription(String primaryLabel) {
|
||||
return Optional.ofNullable(this.nodeDescriptionsByPrimaryLabel.get(primaryLabel));
|
||||
|
||||
@@ -40,11 +40,16 @@ import org.springframework.data.neo4j.core.cypher.StatementBuilder.OngoingMatchA
|
||||
public interface Schema {
|
||||
|
||||
/**
|
||||
* Registers and scans the given set of classes to be available as Neo4j domain entities.
|
||||
* Registers the given set of classes to be available as Neo4j domain entities.
|
||||
*
|
||||
* @param entityClasses The additional set of classes to register with this schema
|
||||
* @param initialEntitySet The set of classes to register with this schema
|
||||
*/
|
||||
void register(Set<? extends Class<?>> entityClasses);
|
||||
void setInitialEntitySet(Set<? extends Class<?>> initialEntitySet);
|
||||
|
||||
/**
|
||||
* Triggers the scanning of the registered, initial entity set.
|
||||
*/
|
||||
void initialize();
|
||||
|
||||
/**
|
||||
* Retrieves a nodes description by its primary label.
|
||||
|
||||
@@ -23,18 +23,12 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.AccessMode;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.Session;
|
||||
import org.neo4j.driver.TransactionConfig;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.data.neo4j.core.NodeManagerFactory;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.IllegalTransactionStateException;
|
||||
import org.springframework.transaction.InvalidIsolationLevelException;
|
||||
@@ -57,32 +51,14 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
@Slf4j
|
||||
public class Neo4jTransactionManager extends AbstractPlatformTransactionManager implements BeanFactoryAware {
|
||||
public class Neo4jTransactionManager extends AbstractPlatformTransactionManager {
|
||||
|
||||
private final Driver driver;
|
||||
|
||||
@Nullable
|
||||
private NodeManagerFactory nodeManagerFactory;
|
||||
|
||||
public Neo4jTransactionManager(Driver driver) {
|
||||
this.driver = driver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
try {
|
||||
this.setNodeManagerFactory(beanFactory.getBean(NodeManagerFactory.class));
|
||||
} catch (NoSuchBeanDefinitionException ex) {
|
||||
log.warn("Found no instance of {}", NodeManagerFactory.class);
|
||||
}
|
||||
}
|
||||
|
||||
public void setNodeManagerFactory(@Nullable NodeManagerFactory nodeManagerFactory) {
|
||||
|
||||
// TODO Check if the NodeManager uses the same datasource aka driver as we do
|
||||
this.nodeManagerFactory = nodeManagerFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object doGetTransaction() throws TransactionException {
|
||||
|
||||
@@ -118,13 +94,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
|
||||
connectionHolder.setSynchronizedWithTransaction(true);
|
||||
transactionObject.setResourceHolder(connectionHolder);
|
||||
TransactionSynchronizationManager.bindResource(this.driver, connectionHolder);
|
||||
|
||||
if (this.nodeManagerFactory != null) {
|
||||
NodeManagerHolder nodeManagerHolder = new NodeManagerHolder(this.nodeManagerFactory.createNodeManager());
|
||||
nodeManagerHolder.setSynchronizedWithTransaction(true);
|
||||
transactionObject.setNodeManagerHolder(nodeManagerHolder);
|
||||
TransactionSynchronizationManager.bindResource(this.nodeManagerFactory, nodeManagerHolder);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
throw new TransactionSystemException(String.format("Could not open a new Neo4j session: %s", ex.getMessage()));
|
||||
@@ -173,14 +142,8 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
|
||||
|
||||
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction);
|
||||
transactionObject.getRequiredResourceHolder().close();
|
||||
transactionObject.getNodeManagerHolder()
|
||||
.map(NodeManagerHolder::getNodeManager)
|
||||
.ifPresent(nodeManager -> nodeManager.flush());
|
||||
|
||||
TransactionSynchronizationManager.unbindResource(driver);
|
||||
if (this.nodeManagerFactory != null) {
|
||||
TransactionSynchronizationManager.unbindResource(this.nodeManagerFactory);
|
||||
}
|
||||
}
|
||||
|
||||
private static TransactionConfig createTransactionConfigFrom(TransactionDefinition definition) {
|
||||
@@ -227,9 +190,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
|
||||
@Nullable
|
||||
private Neo4jConnectionHolder resourceHolder;
|
||||
|
||||
@Nullable
|
||||
private NodeManagerHolder nodeManagerHolder;
|
||||
|
||||
Neo4jTransactionObject(@Nullable Neo4jConnectionHolder resourceHolder) {
|
||||
this.resourceHolder = resourceHolder;
|
||||
}
|
||||
@@ -244,10 +204,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
|
||||
this.resourceHolder = resourceHolder;
|
||||
}
|
||||
|
||||
void setNodeManagerHolder(@Nullable NodeManagerHolder nodeManagerHolder) {
|
||||
this.nodeManagerHolder = nodeManagerHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if a {@link Neo4jConnectionHolder} is set.
|
||||
*/
|
||||
@@ -261,10 +217,6 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
|
||||
return resourceHolder;
|
||||
}
|
||||
|
||||
Optional<NodeManagerHolder> getNodeManagerHolder() {
|
||||
return Optional.ofNullable(nodeManagerHolder);
|
||||
}
|
||||
|
||||
void setRollbackOnly() {
|
||||
|
||||
getRequiredResourceHolder().setRollbackOnly();
|
||||
|
||||
@@ -29,8 +29,6 @@ import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.SessionParametersTemplate;
|
||||
import org.neo4j.driver.Transaction;
|
||||
import org.neo4j.driver.reactive.RxTransaction;
|
||||
import org.springframework.data.neo4j.core.NodeManager;
|
||||
import org.springframework.data.neo4j.core.NodeManagerFactory;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
@@ -48,12 +46,10 @@ public final class Neo4jTransactionUtils {
|
||||
* @return Session parameters to configure the default session used
|
||||
*/
|
||||
public static Consumer<SessionParametersTemplate> defaultSessionParameters(@Nullable String databaseName) {
|
||||
return t -> {
|
||||
t
|
||||
.withDefaultAccessMode(AccessMode.WRITE)
|
||||
.withBookmarks(Collections.EMPTY_LIST)
|
||||
.withDatabase(Optional.ofNullable(databaseName).orElse(DEFAULT_DATABASE_NAME));
|
||||
};
|
||||
return t -> t
|
||||
.withDefaultAccessMode(AccessMode.WRITE)
|
||||
.withBookmarks(Collections.EMPTY_LIST)
|
||||
.withDatabase(Optional.ofNullable(databaseName).orElse(DEFAULT_DATABASE_NAME));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,33 +101,6 @@ public final class Neo4jTransactionUtils {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
public static NodeManager retrieveTransactionalNodeManager(NodeManagerFactory nodeManagerFactory) {
|
||||
|
||||
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
return nodeManagerFactory.createNodeManager();
|
||||
}
|
||||
|
||||
// Try existing transaction
|
||||
NodeManagerHolder nodeManagerHolder = (NodeManagerHolder) TransactionSynchronizationManager
|
||||
.getResource(nodeManagerFactory);
|
||||
|
||||
if (nodeManagerHolder != null) {
|
||||
|
||||
return nodeManagerHolder.getNodeManager();
|
||||
}
|
||||
|
||||
// Manually create a new synchronization
|
||||
nodeManagerHolder = new NodeManagerHolder(nodeManagerFactory.createNodeManager());
|
||||
nodeManagerHolder.setSynchronizedWithTransaction(true);
|
||||
|
||||
TransactionSynchronizationManager.registerSynchronization(
|
||||
new NodeManagerSynchronization(nodeManagerHolder, nodeManagerFactory));
|
||||
|
||||
TransactionSynchronizationManager.bindResource(nodeManagerFactory, nodeManagerHolder);
|
||||
|
||||
return nodeManagerHolder.getNodeManager();
|
||||
}
|
||||
|
||||
private Neo4jTransactionUtils() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core.transaction;
|
||||
|
||||
import org.springframework.data.neo4j.core.NodeManager;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.support.ResourceHolderSupport;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Dedicated holder for storing a NodeManager inside a transaction.
|
||||
* <p>
|
||||
* <strong>Note:</strong> Intended for internal usage only.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
class NodeManagerHolder extends ResourceHolderSupport {
|
||||
|
||||
@Nullable
|
||||
private final NodeManager nodeManager;
|
||||
|
||||
NodeManagerHolder(@Nullable NodeManager nodeManager) {
|
||||
this.nodeManager = nodeManager;
|
||||
}
|
||||
|
||||
public NodeManager getNodeManager() {
|
||||
Assert.state(this.nodeManager != null, "No NodeManager available");
|
||||
return this.nodeManager;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core.transaction;
|
||||
|
||||
import org.springframework.data.neo4j.core.NodeManagerFactory;
|
||||
import org.springframework.transaction.support.ResourceHolderSynchronization;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
public class NodeManagerSynchronization
|
||||
extends ResourceHolderSynchronization<NodeManagerHolder, Object> {
|
||||
|
||||
private final NodeManagerHolder localNodeManagerHolder;
|
||||
|
||||
NodeManagerSynchronization(NodeManagerHolder nodeManagerHolder, NodeManagerFactory nodeManagerFactory) {
|
||||
|
||||
super(nodeManagerHolder, nodeManagerFactory);
|
||||
this.localNodeManagerHolder = nodeManagerHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void flushResource(NodeManagerHolder resourceHolder) {
|
||||
resourceHolder.getNodeManager().flush();
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core;
|
||||
package org.springframework.data.neo4j.repository;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.data.neo4j.core.PersistenceException.IllegalResultSizeException;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
|
||||
/**
|
||||
* Throw when a query doesn't return a required result.
|
||||
@@ -29,9 +29,16 @@ import org.springframework.data.neo4j.core.PersistenceException.IllegalResultSiz
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public class NoResultException extends IllegalResultSizeException {
|
||||
public class NoResultException extends EmptyResultDataAccessException {
|
||||
|
||||
NoResultException(long expectedNumberOfResults, String query) {
|
||||
super(expectedNumberOfResults, 0L, query);
|
||||
private final String query;
|
||||
|
||||
public NoResultException(int expectedNumberOfResults, String query) {
|
||||
super(expectedNumberOfResults);
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
}
|
||||
@@ -31,9 +31,7 @@ import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.data.neo4j.core.NodeManager;
|
||||
import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
/**
|
||||
* Annotation to activate Neo4j repositories. If no base package is configured through either {@link #value()},
|
||||
@@ -78,15 +76,14 @@ public @interface EnableNeo4jRepositories {
|
||||
Class<?> repositoryFactoryBeanClass() default Neo4jRepositoryFactoryBean.class;
|
||||
|
||||
/**
|
||||
* Configures the name of the {@link NodeManager} bean to be used with the repositories detected.
|
||||
* Configures the name of the {@link org.springframework.data.neo4j.core.mapping.Neo4jMappingContext} bean to be used with the repositories detected.
|
||||
*/
|
||||
String nodeManagerFactoryRef() default DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME;
|
||||
String neo4jMappingContextRef() default DEFAULT_MAPPING_CONTEXT_BEAN_NAME;
|
||||
|
||||
/**
|
||||
* Configures the name of the {@link PlatformTransactionManager} bean definition to be used to create repositories
|
||||
* discovered through this annotation. Defaults to {@code transactionManager}.
|
||||
* Configures the name of the {@link org.springframework.data.neo4j.core.Neo4jClient} bean to be used with the repositories detected.
|
||||
*/
|
||||
String transactionManagerRef() default DEFAULT_TRANSACTION_MANAGER_BEAN_NAME;
|
||||
String neo4jClientRef() default DEFAULT_NEO4J_CLIENT_NAME;
|
||||
|
||||
/**
|
||||
* Specifies which types are eligible for component scanning. Further narrows the set of candidate components from
|
||||
|
||||
@@ -24,12 +24,9 @@ import java.util.Collections;
|
||||
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
import org.springframework.data.neo4j.core.schema.Node;
|
||||
import org.springframework.data.neo4j.repository.Neo4jRepository;
|
||||
import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean;
|
||||
import org.springframework.data.neo4j.repository.support.NodeManagerFactoryBean;
|
||||
import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
|
||||
import org.springframework.data.repository.config.RepositoryConfigurationSource;
|
||||
|
||||
@@ -49,20 +46,12 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati
|
||||
/**
|
||||
* See {@link AbstractBeanDefinition#INFER_METHOD}.
|
||||
*/
|
||||
static final String GENERATE_BEAN_NAME = "(generated)";
|
||||
|
||||
static final String DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME = "nodeManagerFactory";
|
||||
static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager";
|
||||
static final String DEFAULT_NEO4J_CLIENT_NAME = "neo4jClient";
|
||||
|
||||
/**
|
||||
* Holds the name of the shared NodeManagerBean created from the factory with the configured name.
|
||||
* See {@link AbstractBeanDefinition#INFER_METHOD}.
|
||||
*/
|
||||
private String generatedNodeManagerBeanName;
|
||||
|
||||
/**
|
||||
* Holds the name of the shared Neo4j mapping context..
|
||||
*/
|
||||
private String generatedMappingContextBeanName;
|
||||
static final String DEFAULT_MAPPING_CONTEXT_BEAN_NAME = "neo4jMappingContext";
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
@@ -99,61 +88,9 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati
|
||||
@Override
|
||||
public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) {
|
||||
|
||||
builder.addPropertyValue("transactionManager",
|
||||
source.getAttribute("transactionManagerRef").orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME));
|
||||
|
||||
builder.addPropertyReference("nodeManager", this.generatedNodeManagerBeanName);
|
||||
builder.addPropertyReference("neo4jMappingContext", this.generatedMappingContextBeanName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerBeansForRoot(BeanDefinitionRegistry registry,
|
||||
RepositoryConfigurationSource config) {
|
||||
|
||||
// Mapping context
|
||||
AbstractBeanDefinition neo4jMappingContextBeanDefinition = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(Neo4jMappingContext.class)
|
||||
.getBeanDefinition();
|
||||
this.generatedMappingContextBeanName = registerWithSourceAndGeneratedBeanName(
|
||||
neo4jMappingContextBeanDefinition, registry, config);
|
||||
|
||||
// Augmented node manager factory (creating injectable, shared instances of NodeManager that are aware of the mapping context).
|
||||
String nameOfNodeManagerFactory = config.getAttribute("nodeManagerFactoryRef")
|
||||
.orElse(DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME);
|
||||
AbstractBeanDefinition sharedSessionCreatorBeanDefinition = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(NodeManagerFactoryBean.class)
|
||||
.addConstructorArgReference(nameOfNodeManagerFactory)
|
||||
.addConstructorArgReference(generatedMappingContextBeanName)
|
||||
.getBeanDefinition();
|
||||
this.generatedNodeManagerBeanName = registerWithSourceAndGeneratedBeanName(
|
||||
sharedSessionCreatorBeanDefinition, registry, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a generated bean name if {@code configuredBeanName} is equal to {@link #GENERATE_BEAN_NAME}, otherwise uses
|
||||
* the configured bean name to register the new bean. Does not check if there's already a bean under the configured
|
||||
* name but throws a {@link org.springframework.beans.factory.BeanDefinitionStoreException}. Checks whether a bean is
|
||||
* already registered under {@code configuredBeanName} in the given {@link BeanDefinitionRegistry} and uses a
|
||||
* generated name for registering the bean instead. If not, the suggested bean name is used.
|
||||
*
|
||||
* @param bean must not be {@literal null}.
|
||||
* @param registry must not be {@literal null}.
|
||||
* @param configuredBeanName must not be {@literal null} or empty.
|
||||
* @param source must not be {@literal null}.
|
||||
* @return the bean name used for registering the given {@link AbstractBeanDefinition}
|
||||
* @throws org.springframework.beans.factory.BeanDefinitionStoreException if the BeanDefinition is invalid or if there
|
||||
* is already a BeanDefinition for the specified bean name * (and we are not allowed to override it)
|
||||
*/
|
||||
private static String registerWithGeneratedNameOrUseConfigured(AbstractBeanDefinition bean,
|
||||
BeanDefinitionRegistry registry, String configuredBeanName, Object source) {
|
||||
|
||||
String registeredBeanName = configuredBeanName;
|
||||
if (GENERATE_BEAN_NAME.equals(configuredBeanName)) {
|
||||
registeredBeanName = registerWithSourceAndGeneratedBeanName(bean, registry, source);
|
||||
} else {
|
||||
bean.setSource(source);
|
||||
registry.registerBeanDefinition(configuredBeanName, bean);
|
||||
}
|
||||
return registeredBeanName;
|
||||
builder.addPropertyReference("neo4jClient",
|
||||
source.getAttribute("neo4jClientRef").orElse(DEFAULT_NEO4J_CLIENT_NAME));
|
||||
builder.addPropertyReference("neo4jMappingContext",
|
||||
source.getAttribute("neo4jMappingContextRef").orElse(DEFAULT_MAPPING_CONTEXT_BEAN_NAME));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.geo.Circle;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Metrics;
|
||||
import org.springframework.data.neo4j.core.NodeManager;
|
||||
import org.springframework.data.neo4j.core.PreparedQuery;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
import org.springframework.data.neo4j.repository.support.PreparedQuery;
|
||||
import org.springframework.data.repository.query.QueryMethod;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -41,19 +41,19 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
abstract class AbstractNeo4jQuery implements RepositoryQuery {
|
||||
|
||||
protected final NodeManager nodeManager;
|
||||
protected final Neo4jClient neo4jClient;
|
||||
protected final Neo4jMappingContext mappingContext;
|
||||
protected final Neo4jQueryMethod queryMethod;
|
||||
protected final Class<?> domainType;
|
||||
|
||||
AbstractNeo4jQuery(NodeManager nodeManager,
|
||||
AbstractNeo4jQuery(Neo4jClient neo4jClient,
|
||||
Neo4jMappingContext mappingContext, Neo4jQueryMethod queryMethod) {
|
||||
|
||||
Assert.notNull(nodeManager, "The node manager is required.");
|
||||
Assert.notNull(neo4jClient, "The Neo4j client is required.");
|
||||
Assert.notNull(mappingContext, "The mapping context is required.");
|
||||
Assert.notNull(queryMethod, "Query method must not be null!");
|
||||
|
||||
this.nodeManager = nodeManager;
|
||||
this.neo4jClient = neo4jClient;
|
||||
this.mappingContext = mappingContext;
|
||||
this.queryMethod = queryMethod;
|
||||
this.domainType = queryMethod.getReturnedObjectType();
|
||||
@@ -66,7 +66,7 @@ abstract class AbstractNeo4jQuery implements RepositoryQuery {
|
||||
|
||||
@Override
|
||||
public final Object execute(Object[] parameters) {
|
||||
return new Neo4jQueryExecution.DefaultQueryExecution(nodeManager)
|
||||
return new Neo4jQueryExecution.DefaultQueryExecution(neo4jClient)
|
||||
.execute(prepareQuery(parameters), queryMethod.isCollectionQuery());
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ package org.springframework.data.neo4j.repository.query;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.data.neo4j.core.ExecutableQuery;
|
||||
import org.springframework.data.neo4j.core.NodeManager;
|
||||
import org.springframework.data.neo4j.core.PreparedQuery;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
import org.springframework.data.neo4j.repository.support.ExecutableQuery;
|
||||
import org.springframework.data.neo4j.repository.support.PreparedQuery;
|
||||
|
||||
/**
|
||||
* Set of classes to contain query execution strategies. Depending (mostly) on the return type of a
|
||||
@@ -40,12 +40,12 @@ interface Neo4jQueryExecution {
|
||||
@RequiredArgsConstructor
|
||||
class DefaultQueryExecution implements Neo4jQueryExecution {
|
||||
|
||||
private final NodeManager nodeManager;
|
||||
private final Neo4jClient neo4jClient;
|
||||
|
||||
@Override
|
||||
public Object execute(PreparedQuery preparedQuery, boolean asCollectionQuery) {
|
||||
public Object execute(PreparedQuery description, boolean asCollectionQuery) {
|
||||
|
||||
ExecutableQuery executableQuery = nodeManager.toExecutableQuery(preparedQuery);
|
||||
ExecutableQuery executableQuery = ExecutableQuery.create(description, neo4jClient);
|
||||
if (asCollectionQuery) {
|
||||
return executableQuery.getResults();
|
||||
} else {
|
||||
|
||||
@@ -23,7 +23,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.data.neo4j.core.NodeManager;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.repository.core.NamedQueries;
|
||||
@@ -43,7 +43,7 @@ import org.springframework.data.repository.query.RepositoryQuery;
|
||||
@RequiredArgsConstructor
|
||||
public final class Neo4jQueryLookupStrategy implements QueryLookupStrategy {
|
||||
|
||||
private final NodeManager nodeManager;
|
||||
private final Neo4jClient neo4jClient;
|
||||
private final Neo4jMappingContext mappingContext;
|
||||
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
|
||||
|
||||
@@ -58,12 +58,12 @@ public final class Neo4jQueryLookupStrategy implements QueryLookupStrategy {
|
||||
String namedQueryName = queryMethod.getNamedQueryName();
|
||||
|
||||
if (namedQueries.hasQuery(namedQueryName)) {
|
||||
return StringBasedNeo4jQuery.create(nodeManager, mappingContext, evaluationContextProvider, queryMethod,
|
||||
return StringBasedNeo4jQuery.create(neo4jClient, mappingContext, evaluationContextProvider, queryMethod,
|
||||
namedQueries.getQuery(namedQueryName));
|
||||
} else if (queryMethod.hasQueryAnnotation()) {
|
||||
return StringBasedNeo4jQuery.create(nodeManager, mappingContext, evaluationContextProvider, queryMethod);
|
||||
return StringBasedNeo4jQuery.create(neo4jClient, mappingContext, evaluationContextProvider, queryMethod);
|
||||
} else {
|
||||
return new PartTreeNeo4jQuery(nodeManager, mappingContext, queryMethod);
|
||||
return new PartTreeNeo4jQuery(neo4jClient, mappingContext, queryMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,10 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.neo4j.driver.types.Point;
|
||||
import org.springframework.data.neo4j.core.NodeManager;
|
||||
import org.springframework.data.neo4j.core.PreparedQuery;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod.Neo4jParameters;
|
||||
import org.springframework.data.neo4j.repository.support.PreparedQuery;
|
||||
import org.springframework.data.repository.query.ParameterAccessor;
|
||||
import org.springframework.data.repository.query.ParametersParameterAccessor;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
@@ -75,11 +75,11 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery {
|
||||
private final PartTree tree;
|
||||
|
||||
PartTreeNeo4jQuery(
|
||||
NodeManager nodeManager,
|
||||
Neo4jClient neo4jClient,
|
||||
Neo4jMappingContext mappingContext,
|
||||
Neo4jQueryMethod queryMethod
|
||||
) {
|
||||
super(nodeManager, mappingContext, queryMethod);
|
||||
super(neo4jClient, mappingContext, queryMethod);
|
||||
|
||||
this.tree = new PartTree(queryMethod.getName(), domainType);
|
||||
|
||||
@@ -97,7 +97,6 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery {
|
||||
mappingContext, domainType, tree, formalParameters, actualParameters
|
||||
);
|
||||
|
||||
|
||||
String cypherQuery = queryCreator.createQuery();
|
||||
Map<String, Object> boundedParameters = formalParameters
|
||||
.getBindableParameters().stream()
|
||||
|
||||
@@ -23,9 +23,9 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.neo4j.core.NodeManager;
|
||||
import org.springframework.data.neo4j.core.PreparedQuery;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
import org.springframework.data.neo4j.repository.support.PreparedQuery;
|
||||
import org.springframework.data.repository.query.Parameter;
|
||||
import org.springframework.data.repository.query.Parameters;
|
||||
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
|
||||
@@ -92,13 +92,13 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery {
|
||||
* Create a {@link StringBasedNeo4jQuery} for a query method that is annotated with {@link Query @Query}. The annotation
|
||||
* is expected to have a value.
|
||||
*
|
||||
* @param nodeManager
|
||||
* @param neo4jClient
|
||||
* @param mappingContext
|
||||
* @param evaluationContextProvider
|
||||
* @param queryMethod
|
||||
* @return A new instance of a String based Neo4j query.
|
||||
*/
|
||||
static StringBasedNeo4jQuery create(NodeManager nodeManager, Neo4jMappingContext mappingContext,
|
||||
static StringBasedNeo4jQuery create(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider,
|
||||
Neo4jQueryMethod queryMethod) {
|
||||
|
||||
@@ -109,36 +109,36 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery {
|
||||
.filter(StringUtils::hasText)
|
||||
.orElseThrow(() -> new MappingException("Expected @Query annotation to have a value, but it did not."));
|
||||
|
||||
return new StringBasedNeo4jQuery(nodeManager, mappingContext, evaluationContextProvider, queryMethod,
|
||||
return new StringBasedNeo4jQuery(neo4jClient, mappingContext, evaluationContextProvider, queryMethod,
|
||||
cypherTemplate, queryAnnotation.count(), queryAnnotation.exists(), queryAnnotation.delete());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link StringBasedNeo4jQuery} based on an explicit Cypher template.
|
||||
*
|
||||
* @param nodeManager
|
||||
* @param neo4jClient
|
||||
* @param mappingContext
|
||||
* @param evaluationContextProvider
|
||||
* @param queryMethod
|
||||
* @param cypherTemplate The template to use.
|
||||
* @return A new instance of a String based Neo4j query.
|
||||
*/
|
||||
static StringBasedNeo4jQuery create(NodeManager nodeManager, Neo4jMappingContext mappingContext,
|
||||
static StringBasedNeo4jQuery create(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider,
|
||||
Neo4jQueryMethod queryMethod, String cypherTemplate) {
|
||||
|
||||
Assert.hasText(cypherTemplate, "Cannot create String based Neo4j query without a cypher template.");
|
||||
|
||||
return new StringBasedNeo4jQuery(nodeManager, mappingContext, evaluationContextProvider, queryMethod,
|
||||
return new StringBasedNeo4jQuery(neo4jClient, mappingContext, evaluationContextProvider, queryMethod,
|
||||
cypherTemplate, false, false, false);
|
||||
}
|
||||
|
||||
private StringBasedNeo4jQuery(NodeManager nodeManager,
|
||||
private StringBasedNeo4jQuery(Neo4jClient neo4jClient,
|
||||
Neo4jMappingContext mappingContext, QueryMethodEvaluationContextProvider evaluationContextProvider,
|
||||
Neo4jQueryMethod queryMethod, String cypherTemplate, boolean countQuery,
|
||||
boolean existsQuery, boolean deleteQuery) {
|
||||
|
||||
super(nodeManager, mappingContext, queryMethod);
|
||||
super(neo4jClient, mappingContext, queryMethod);
|
||||
|
||||
this.countQuery = countQuery;
|
||||
this.existsQuery = existsQuery;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.repository.support;
|
||||
|
||||
import static java.util.stream.Collectors.*;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.neo4j.driver.exceptions.NoSuchRecordException;
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
import org.springframework.data.neo4j.repository.NoResultException;
|
||||
|
||||
@RequiredArgsConstructor public
|
||||
final class DefaultExecutableQuery<T> implements ExecutableQuery<T> {
|
||||
|
||||
private final PreparedQuery<T> preparedQuery;
|
||||
private final Neo4jClient.RecordFetchSpec<Optional<T>, Collection<T>, T> fetchSpec;
|
||||
|
||||
@Override
|
||||
public List<T> getResults() {
|
||||
return fetchSpec.all().stream().collect(toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<T> getSingleResult() {
|
||||
try {
|
||||
return fetchSpec.one();
|
||||
} catch (NoSuchRecordException e) {
|
||||
// This exception is thrown by the driver in both cases when there are 0 or 1+n records
|
||||
// So there has been an incorrect result size, but not to few results but to many.
|
||||
throw new IncorrectResultSizeDataAccessException(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getRequiredSingleResult() {
|
||||
return fetchSpec.one()
|
||||
.orElseThrow(() -> new NoResultException(1, preparedQuery.getCypherQuery()));
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core;
|
||||
package org.springframework.data.neo4j.repository.support;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -27,9 +27,9 @@ import org.neo4j.driver.types.TypeSystem;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Typed preparation of a query that is used to create an {@link org.springframework.data.neo4j.core.ExecutableQuery} of the same type.
|
||||
* Typed preparation of a query that is used to create an {@link ExecutableQuery} of the same type.
|
||||
* <p/>
|
||||
* When no mapping function is provided, the node manager will assume a simple type to be returned. Otherwise make sure
|
||||
* When no mapping function is provided, the Neo4j client will assume a simple type to be returned. Otherwise make sure
|
||||
* that the query fits to the mapping function, that is: It must return all nodes, relationships and paths that is expected
|
||||
* by the mapping function to work correctly.
|
||||
*
|
||||
@@ -16,12 +16,16 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core;
|
||||
package org.springframework.data.neo4j.repository.support;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
import org.springframework.data.neo4j.repository.NoResultException;
|
||||
|
||||
/**
|
||||
* Interface for controlling query execution.
|
||||
@@ -31,9 +35,23 @@ import org.apiguardian.api.API;
|
||||
* @soundtrack Deichkind - Niveau weshalb warum
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public interface ExecutableQuery<T> {
|
||||
|
||||
static <T> ExecutableQuery<T> create(PreparedQuery<T> description, Neo4jClient neo4jClient) {
|
||||
|
||||
Class<T> resultType = description.getResultType();
|
||||
Neo4jClient.MappingSpec<Optional<T>, Collection<T>, T> mappingSpec = neo4jClient
|
||||
.newQuery(description.getCypherQuery())
|
||||
.bindAll(description.getParameters())
|
||||
.fetchAs(resultType);
|
||||
Neo4jClient.RecordFetchSpec<Optional<T>, Collection<T>, T> fetchSpec = description.getOptionalMappingFunction()
|
||||
.map(mappingFunction -> mappingSpec.mappedBy(mappingFunction))
|
||||
.orElse(mappingSpec);
|
||||
|
||||
return new DefaultExecutableQuery<>(description, fetchSpec);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return All results returned by this query.
|
||||
*/
|
||||
@@ -41,13 +59,12 @@ public interface ExecutableQuery<T> {
|
||||
|
||||
/**
|
||||
* @return A single result
|
||||
* @throws NonUniqueResultException
|
||||
* @throws IncorrectResultSizeDataAccessException
|
||||
*/
|
||||
Optional<T> getSingleResult();
|
||||
|
||||
/**
|
||||
* @return A single result
|
||||
* @throws NonUniqueResultException
|
||||
* @throws NoResultException
|
||||
*/
|
||||
T getRequiredSingleResult();
|
||||
@@ -20,7 +20,7 @@ package org.springframework.data.neo4j.repository.support;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.neo4j.core.NodeManager;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
import org.springframework.data.neo4j.repository.Neo4jRepository;
|
||||
import org.springframework.data.neo4j.repository.query.Neo4jQueryLookupStrategy;
|
||||
@@ -41,12 +41,12 @@ import org.springframework.data.repository.query.QueryMethodEvaluationContextPro
|
||||
*/
|
||||
final class Neo4jRepositoryFactory extends RepositoryFactorySupport {
|
||||
|
||||
private final NodeManager nodeManager;
|
||||
private final Neo4jClient neo4jClient;
|
||||
|
||||
private final Neo4jMappingContext mappingContext;
|
||||
|
||||
Neo4jRepositoryFactory(NodeManager nodeManager, Neo4jMappingContext mappingContext) {
|
||||
this.nodeManager = nodeManager;
|
||||
Neo4jRepositoryFactory(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext) {
|
||||
this.neo4jClient = neo4jClient;
|
||||
this.mappingContext = mappingContext;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport {
|
||||
|
||||
@Override
|
||||
protected Object getTargetRepository(RepositoryInformation metadata) {
|
||||
return getTargetRepositoryViaReflection(metadata, nodeManager, mappingContext, metadata.getDomainType());
|
||||
return getTargetRepositoryViaReflection(metadata, neo4jClient, mappingContext, metadata.getDomainType());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -73,6 +73,6 @@ final class Neo4jRepositoryFactory extends RepositoryFactorySupport {
|
||||
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(Key key,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider) {
|
||||
|
||||
return Optional.of(new Neo4jQueryLookupStrategy(nodeManager, mappingContext, evaluationContextProvider));
|
||||
return Optional.of(new Neo4jQueryLookupStrategy(neo4jClient, mappingContext, evaluationContextProvider));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ package org.springframework.data.neo4j.repository.support;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.data.neo4j.core.NodeManager;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
|
||||
@@ -36,7 +36,7 @@ import org.springframework.data.repository.core.support.TransactionalRepositoryF
|
||||
public class Neo4jRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
|
||||
extends TransactionalRepositoryFactoryBeanSupport<T, S, ID> {
|
||||
|
||||
private NodeManager nodeManager;
|
||||
private Neo4jClient neo4jClient;
|
||||
|
||||
private Neo4jMappingContext neo4jMappingContext;
|
||||
|
||||
@@ -49,8 +49,8 @@ public class Neo4jRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten
|
||||
super(repositoryInterface);
|
||||
}
|
||||
|
||||
public void setNodeManager(NodeManager nodeManager) {
|
||||
this.nodeManager = nodeManager;
|
||||
public void setNeo4jClient(Neo4jClient neo4jClient) {
|
||||
this.neo4jClient = neo4jClient;
|
||||
}
|
||||
|
||||
public void setNeo4jMappingContext(Neo4jMappingContext neo4jMappingContext) {
|
||||
@@ -59,7 +59,7 @@ public class Neo4jRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten
|
||||
|
||||
@Override
|
||||
protected RepositoryFactorySupport doCreateRepositoryFactory() {
|
||||
return new Neo4jRepositoryFactory(nodeManager, neo4jMappingContext);
|
||||
return new Neo4jRepositoryFactory(neo4jClient, neo4jMappingContext);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.repository.support;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.data.neo4j.core.NodeManager;
|
||||
import org.springframework.data.neo4j.core.NodeManagerFactory;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
|
||||
/**
|
||||
* This is a shim that integrates a {@link NodeManagerFactory} with Spring Datas infrastructure. It takes in the factory
|
||||
* that needs to be provided by the user and the mapping context provided by our infrastructure. Springs mapping context
|
||||
* replaces the default noop scanner.
|
||||
*
|
||||
* @author Gerrit Meier
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
@RequiredArgsConstructor
|
||||
public final class NodeManagerFactoryBean implements InitializingBean, FactoryBean<NodeManager> {
|
||||
|
||||
private final NodeManagerFactory target;
|
||||
|
||||
private final Neo4jMappingContext neo4jMappingContext;
|
||||
|
||||
@Override
|
||||
public NodeManager getObject() {
|
||||
return SharedNodeManagerCreator.createSharedNodeManager(this.target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return NodeManager.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
|
||||
target.setSchema(this.neo4jMappingContext);
|
||||
target.initialize();
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core;
|
||||
package org.springframework.data.neo4j.repository.support;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -30,12 +30,19 @@ import org.neo4j.driver.types.TypeSystem;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Typed preparation of a query that is used to create an {@link ExecutableQuery} of the same type.
|
||||
* <p/>
|
||||
* When no mapping function is provided, the Neo4j client will assume a simple type to be returned. Otherwise make sure
|
||||
* that the query fits to the mapping function, that is: It must return all nodes, relationships and paths that is expected
|
||||
* by the mapping function to work correctly.
|
||||
*
|
||||
* @param <T> The type of the objects returned by this query.
|
||||
* @see ExecutableQuery#create(PreparedQuery, org.springframework.data.neo4j.core.Neo4jClient)
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Deichkind - Arbeit nervt
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public interface PreparedQuery<T> {
|
||||
|
||||
static <CT> RequiredBuildStep<CT> queryFor(Class<CT> resultType) {
|
||||
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.repository.support;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.data.neo4j.core.NodeManager;
|
||||
import org.springframework.data.neo4j.core.NodeManagerFactory;
|
||||
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@Slf4j
|
||||
final class SharedNodeManagerCreator {
|
||||
|
||||
private static final Set<String> TRANSACTION_REQUIRING_METHODS;
|
||||
|
||||
static {
|
||||
Set<String> tmp = new HashSet<>();
|
||||
|
||||
tmp.add("save");
|
||||
tmp.add("delete");
|
||||
|
||||
TRANSACTION_REQUIRING_METHODS = Collections.unmodifiableSet(tmp);
|
||||
}
|
||||
|
||||
public static NodeManager createSharedNodeManager(NodeManagerFactory nodeManagerFactory) {
|
||||
|
||||
return (NodeManager) Proxy.newProxyInstance(SharedNodeManagerCreator.class.getClassLoader(),
|
||||
new Class<?>[] { NodeManager.class }, new SharedNodeManagerInvocationHandler(nodeManagerFactory));
|
||||
}
|
||||
|
||||
private static class SharedNodeManagerInvocationHandler implements InvocationHandler, Serializable {
|
||||
|
||||
private final NodeManagerFactory targetFactory;
|
||||
|
||||
SharedNodeManagerInvocationHandler(NodeManagerFactory targetFactory) {
|
||||
this.targetFactory = targetFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
|
||||
String methodName = method.getName();
|
||||
switch (methodName) {
|
||||
case "equals":
|
||||
// Only consider equal when proxies are identical.
|
||||
return (proxy == args[0]);
|
||||
case "hashCode":
|
||||
// Use hashCode of proxy.
|
||||
return hashCode();
|
||||
case "toString":
|
||||
// Deliver toString without touching a target Session.
|
||||
return "Shared Session proxy for target factory [" + this.targetFactory + "]";
|
||||
default:
|
||||
Function<NodeManager, Object> methodCall =
|
||||
targetSession -> ReflectionUtils.invokeMethod(method, targetSession, args);
|
||||
return invokeInTransaction(methodName, methodCall);
|
||||
}
|
||||
}
|
||||
|
||||
private Object invokeInTransaction(String methodName, Function<NodeManager, Object> methodCall) {
|
||||
|
||||
// Determine current Session: either the transactional one
|
||||
// managed by the factory or a temporary one for the given invocation.
|
||||
NodeManager target = Neo4jTransactionUtils.retrieveTransactionalNodeManager(this.targetFactory);
|
||||
|
||||
if (TRANSACTION_REQUIRING_METHODS.contains(methodName)) {
|
||||
if (target == null || (!TransactionSynchronizationManager.isActualTransactionActive()
|
||||
&& target.getTransaction() != null)) {
|
||||
throw new IllegalStateException("No NodeManager with actual transaction available "
|
||||
+ "for current thread - cannot reliably process '" + methodName + "' call");
|
||||
}
|
||||
}
|
||||
|
||||
// Regular Session operations.
|
||||
boolean isNewSession = false;
|
||||
if (target == null) {
|
||||
log.debug("Creating new Session for shared Session invocation");
|
||||
target = this.targetFactory.createNodeManager();
|
||||
isNewSession = true;
|
||||
}
|
||||
|
||||
// Invoke method on current Session.
|
||||
try {
|
||||
return methodCall.apply(target);
|
||||
} finally {
|
||||
if (isNewSession) {
|
||||
target.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SharedNodeManagerCreator() {
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
package org.springframework.data.neo4j.repository.support;
|
||||
|
||||
import static java.util.Collections.*;
|
||||
import static java.util.stream.Collectors.*;
|
||||
import static lombok.AccessLevel.*;
|
||||
import static org.springframework.data.neo4j.core.cypher.Cypher.*;
|
||||
import static org.springframework.data.neo4j.core.schema.NodeDescription.*;
|
||||
@@ -45,8 +46,7 @@ import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.neo4j.core.NodeManager;
|
||||
import org.springframework.data.neo4j.core.PreparedQuery;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
import org.springframework.data.neo4j.core.cypher.Condition;
|
||||
import org.springframework.data.neo4j.core.cypher.Conditions;
|
||||
import org.springframework.data.neo4j.core.cypher.Cypher;
|
||||
@@ -81,7 +81,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
|
||||
private static final Renderer renderer = CypherRenderer.create();
|
||||
|
||||
private final NodeManager nodeManager;
|
||||
private final Neo4jClient neo4jClient;
|
||||
|
||||
private final Neo4jMappingContext mappingContext;
|
||||
|
||||
@@ -91,9 +91,9 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
private final BiFunction<TypeSystem, Record, ?> mappingFunction;
|
||||
private Expression idExpression;
|
||||
|
||||
SimpleNeo4jRepository(NodeManager nodeManager, Neo4jMappingContext mappingContext, Class<T> nodeClass) {
|
||||
SimpleNeo4jRepository(Neo4jClient neo4jClient, Neo4jMappingContext mappingContext, Class<T> nodeClass) {
|
||||
|
||||
this.nodeManager = nodeManager;
|
||||
this.neo4jClient = neo4jClient;
|
||||
this.mappingContext = mappingContext;
|
||||
this.nodeClass = nodeClass;
|
||||
|
||||
@@ -124,7 +124,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
.orderBy(toSortItems(nodeDescription, sort))
|
||||
.build();
|
||||
|
||||
return nodeManager.toExecutableQuery(prepareQuery(statement)).getResults();
|
||||
return createExecutableQuery(statement).getResults();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -137,7 +137,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
|
||||
Statement statement = returningWithPaging.build();
|
||||
|
||||
List<T> allResult = nodeManager.toExecutableQuery(prepareQuery(statement)).getResults();
|
||||
List<T> allResult = createExecutableQuery(statement).getResults();
|
||||
LongSupplier totalCountSupplier = this::count;
|
||||
return PageableExecutionUtils.getPage(allResult, pageable, totalCountSupplier);
|
||||
}
|
||||
@@ -153,8 +153,8 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
|
||||
Statement statement = returningWithPaging.build();
|
||||
|
||||
List<S> page = nodeManager
|
||||
.toExecutableQuery(prepareQuery(example.getProbeType(), statement, predicate.parameters)).getResults();
|
||||
List<S> page = createExecutableQuery(example.getProbeType(), statement, predicate.parameters)
|
||||
.getResults();
|
||||
LongSupplier totalCountSupplier = this::count;
|
||||
return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier);
|
||||
}
|
||||
@@ -173,7 +173,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
@Override
|
||||
@Transactional
|
||||
public <S extends T> S save(S entity) {
|
||||
return this.nodeManager.save(entity);
|
||||
throw new UnsupportedOperationException("Not there yet.");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -189,7 +189,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
.prepareMatchOf(nodeDescription, Optional.of(idExpression.isEqualTo(literalOf(id))))
|
||||
.returning(asterisk())
|
||||
.build();
|
||||
return nodeManager.toExecutableQuery(prepareQuery(statement)).getSingleResult();
|
||||
return createExecutableQuery(statement).getSingleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -200,9 +200,9 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
@Override
|
||||
public Iterable<T> findAll() {
|
||||
|
||||
Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty()).returning(asterisk())
|
||||
.build();
|
||||
return nodeManager.toExecutableQuery(prepareQuery(statement)).getResults();
|
||||
Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty())
|
||||
.returning(asterisk()).build();
|
||||
return createExecutableQuery(statement).getResults();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -213,7 +213,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
.returning(asterisk())
|
||||
.build();
|
||||
|
||||
return nodeManager.toExecutableQuery(prepareQuery(nodeClass, statement, singletonMap("ids", ids))).getResults();
|
||||
return createExecutableQuery(nodeClass, statement, singletonMap("ids", ids)).getResults();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -222,8 +222,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
Statement statement = mappingContext.prepareMatchOf(nodeDescription, Optional.empty())
|
||||
.returning(Functions.count(asterisk())).build();
|
||||
|
||||
return nodeManager.toExecutableQuery(prepareQuery(Long.class, statement, Collections.emptyMap()))
|
||||
.getRequiredSingleResult();
|
||||
return createExecutableQuery(Long.class, statement, Collections.emptyMap()).getRequiredSingleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -263,8 +262,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
.returning(asterisk())
|
||||
.build();
|
||||
|
||||
PreparedQuery<S> preparedQuery = prepareQuery(example.getProbeType(), statement, predicate.parameters);
|
||||
return nodeManager.toExecutableQuery(preparedQuery).getSingleResult();
|
||||
return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getSingleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -275,8 +273,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
.returning(asterisk())
|
||||
.build();
|
||||
|
||||
PreparedQuery<S> preparedQuery = prepareQuery(example.getProbeType(), statement, predicate.parameters);
|
||||
return nodeManager.toExecutableQuery(preparedQuery).getResults();
|
||||
return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getResults();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -288,8 +285,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
.returning(asterisk())
|
||||
.orderBy(toSortItems(nodeDescription, sort)).build();
|
||||
|
||||
PreparedQuery<S> preparedQuery = prepareQuery(example.getProbeType(), statement, predicate.parameters);
|
||||
return nodeManager.toExecutableQuery(preparedQuery).getResults();
|
||||
return createExecutableQuery(example.getProbeType(), statement, predicate.parameters).getResults();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -301,8 +297,7 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
.returning(Functions.count(asterisk()))
|
||||
.build();
|
||||
|
||||
PreparedQuery<Long> preparedQuery = prepareQuery(Long.class, statement, predicate.parameters);
|
||||
return nodeManager.toExecutableQuery(preparedQuery).getRequiredSingleResult();
|
||||
return createExecutableQuery(Long.class, statement, predicate.parameters).getRequiredSingleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -419,11 +414,11 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
return predicate;
|
||||
}
|
||||
|
||||
private PreparedQuery<T> prepareQuery(Statement statement) {
|
||||
return prepareQuery(nodeClass, statement, Collections.emptyMap());
|
||||
private ExecutableQuery<T> createExecutableQuery(Statement statement) {
|
||||
return createExecutableQuery(nodeClass, statement, Collections.emptyMap());
|
||||
}
|
||||
|
||||
private <T> PreparedQuery<T> prepareQuery(Class<T> resultType, Statement statement,
|
||||
private <RS> ExecutableQuery<RS> createExecutableQuery(Class<RS> resultType, Statement statement,
|
||||
Map<String, Object> parameters) {
|
||||
|
||||
BiFunction<TypeSystem, Record, ?> mappingFunctionToUse = this.mappingFunction;
|
||||
@@ -431,10 +426,12 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
|
||||
mappingFunctionToUse = mappingContext.getMappingFunctionFor(resultType).orElse(null);
|
||||
}
|
||||
|
||||
return PreparedQuery.queryFor(resultType)
|
||||
PreparedQuery queryDescription = PreparedQuery.queryFor(resultType)
|
||||
.withCypherQuery(renderer.render(statement))
|
||||
.withParameters(parameters)
|
||||
.usingMappingFunction(mappingFunctionToUse)
|
||||
.build();
|
||||
|
||||
return ExecutableQuery.create(queryDescription, neo4jClient);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core.context;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.neo4j.core.context.tracking.EntityTrackingStrategy;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
import org.springframework.data.neo4j.core.schema.Id;
|
||||
import org.springframework.data.neo4j.core.schema.NodeDescription;
|
||||
import org.springframework.data.neo4j.core.schema.Schema;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
class DefaultPersistenceContextTest {
|
||||
|
||||
private Schema schema;
|
||||
private PersistenceContext context;
|
||||
private EntityTrackingStrategy entityTrackingStrategy;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
entityTrackingStrategy = mock(EntityTrackingStrategy.class);
|
||||
when(entityTrackingStrategy.getObjectIdentifier(any()))
|
||||
.thenAnswer(invocation -> System.identityHashCode(invocation.getArguments()[0]));
|
||||
|
||||
Neo4jMappingContext mappingContext = new Neo4jMappingContext();
|
||||
mappingContext.setInitialEntitySet(new HashSet<Class<?>>(Arrays.asList(Something.class)));
|
||||
mappingContext.initialize();
|
||||
schema = mappingContext;
|
||||
|
||||
// override method to return a verifiable mock
|
||||
context = new DefaultPersistenceContext() {
|
||||
@Override
|
||||
EntityTrackingStrategy getEntityTrackingStrategy() {
|
||||
return entityTrackingStrategy;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerAddsEntityToTrackingStrategy() {
|
||||
|
||||
NodeDescription<?> nodeDescription = schema.getRequiredNodeDescription(Something.class);
|
||||
|
||||
context.register(new Something(), nodeDescription);
|
||||
|
||||
verify(entityTrackingStrategy).track(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerTheSameObjectMultipleTimesCallsTrackJustOnce() {
|
||||
|
||||
NodeDescription<?> nodeDescription = schema.getRequiredNodeDescription(Something.class);
|
||||
Something entity = new Something();
|
||||
|
||||
context.register(entity, nodeDescription);
|
||||
context.register(entity, nodeDescription);
|
||||
|
||||
verify(entityTrackingStrategy).track(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerTwoObjectOfTheSameTypeCallsTrackTwice() {
|
||||
|
||||
NodeDescription<?> nodeDescription = schema.getRequiredNodeDescription(Something.class);
|
||||
Something entity1 = new Something();
|
||||
Something entity2 = new Something();
|
||||
|
||||
context.register(entity1, nodeDescription);
|
||||
context.register(entity2, nodeDescription);
|
||||
|
||||
verify(entityTrackingStrategy, times(2)).track(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void triggersDeltaCalculationOnDeltaCall() {
|
||||
|
||||
NodeDescription<?> nodeDescription = schema.getRequiredNodeDescription(Something.class);
|
||||
Something entity = new Something();
|
||||
|
||||
context.register(entity, nodeDescription);
|
||||
context.getEntityChanges(entity);
|
||||
|
||||
verify(entityTrackingStrategy).getAggregatedEntityChangeEvents(entity);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deregisterRemovesEntityFromTracking() {
|
||||
|
||||
NodeDescription<?> nodeDescription = schema.getRequiredNodeDescription(Something.class);
|
||||
Something entity = new Something();
|
||||
|
||||
context.register(entity, nodeDescription);
|
||||
context.deregister(entity);
|
||||
|
||||
verify(entityTrackingStrategy).untrack(entity);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deregisterUnknownEntityDoesNotCallUntrack() {
|
||||
Something entity = new Something();
|
||||
|
||||
context.deregister(entity);
|
||||
|
||||
verify(entityTrackingStrategy, never()).untrack(entity);
|
||||
}
|
||||
|
||||
class Something {
|
||||
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
String value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.core.context.tracking;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.neo4j.core.schema.GraphPropertyDescription;
|
||||
import org.springframework.data.neo4j.core.schema.NodeDescription;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
class EntityComparisonStrategyTest {
|
||||
|
||||
private final GraphPropertyDescription valuePropertyDescription;
|
||||
|
||||
private final GraphPropertyDescription informationPropertyDescription;
|
||||
|
||||
private final GraphPropertyDescription parentPropertyDescription;
|
||||
|
||||
private final NodeDescription description;
|
||||
|
||||
EntityComparisonStrategyTest() {
|
||||
this.valuePropertyDescription = mock(GraphPropertyDescription.class);
|
||||
when(this.valuePropertyDescription.getFieldName()).thenReturn("value");
|
||||
when(this.valuePropertyDescription.getPropertyName()).thenReturn("value");
|
||||
|
||||
this.informationPropertyDescription = mock(GraphPropertyDescription.class);
|
||||
when(this.informationPropertyDescription.getFieldName()).thenReturn("information");
|
||||
when(this.informationPropertyDescription.getPropertyName()).thenReturn("information");
|
||||
|
||||
this.parentPropertyDescription = mock(GraphPropertyDescription.class);
|
||||
when(this.parentPropertyDescription.getFieldName()).thenReturn("parentValue");
|
||||
when(this.parentPropertyDescription.getPropertyName()).thenReturn("parentValue");
|
||||
|
||||
this.description = mock(NodeDescription.class);
|
||||
when(this.description.getPrimaryLabel()).thenReturn("Something");
|
||||
when(this.description.getGraphProperties()).thenReturn(Arrays
|
||||
.asList(this.valuePropertyDescription, this.informationPropertyDescription,
|
||||
this.parentPropertyDescription));
|
||||
when(this.description.getUnderlyingClass()).thenReturn(Something.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void trackSimplePropertyChange() {
|
||||
EntityComparisonStrategy strategy = new EntityComparisonStrategy();
|
||||
Something something = new Something("oldValue");
|
||||
|
||||
strategy.track(something, description);
|
||||
|
||||
String fieldName = "value";
|
||||
String newValue = "newValue";
|
||||
something.value = newValue;
|
||||
|
||||
Collection<EntityChangeEvent> changeEvents = strategy.getAggregatedEntityChangeEvents(something);
|
||||
|
||||
EntityChangeEvent changeEvent = changeEvents.iterator().next();
|
||||
assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName);
|
||||
assertThat(changeEvent.getValue()).isEqualTo(newValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
void trackCollectionPropertyChange() {
|
||||
EntityComparisonStrategy strategy = new EntityComparisonStrategy();
|
||||
Something something = new Something("oldValue");
|
||||
|
||||
strategy.track(something, description);
|
||||
|
||||
String fieldName = "information";
|
||||
something.information.add("additional entry");
|
||||
|
||||
Collection<EntityChangeEvent> changeEvents = strategy.getAggregatedEntityChangeEvents(something);
|
||||
|
||||
EntityChangeEvent changeEvent = changeEvents.iterator().next();
|
||||
assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName);
|
||||
assertThat(changeEvent.getValue()).isInstanceOf(Integer.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void trackCollectionPropertyReorderChange() {
|
||||
EntityComparisonStrategy strategy = new EntityComparisonStrategy();
|
||||
Something something = new Something("blubb");
|
||||
something.information.add("entry 1");
|
||||
something.information.add("entry 2");
|
||||
|
||||
strategy.track(something, description);
|
||||
|
||||
something.information.sort(Comparator.reverseOrder());
|
||||
|
||||
String fieldName = "information";
|
||||
|
||||
Collection<EntityChangeEvent> changeEvents = strategy.getAggregatedEntityChangeEvents(something);
|
||||
|
||||
EntityChangeEvent changeEvent = changeEvents.iterator().next();
|
||||
assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName);
|
||||
assertThat(changeEvent.getValue()).isInstanceOf(Integer.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void trackParentClassPropertyChange() {
|
||||
EntityComparisonStrategy strategy = new EntityComparisonStrategy();
|
||||
Something something = new Something("oldValue");
|
||||
|
||||
strategy.track(something, description);
|
||||
|
||||
String fieldName = "parentValue";
|
||||
String newValue = "newValue";
|
||||
something.parentValue = newValue;
|
||||
|
||||
Collection<EntityChangeEvent> changeEvents = strategy.getAggregatedEntityChangeEvents(something);
|
||||
|
||||
EntityChangeEvent changeEvent = changeEvents.iterator().next();
|
||||
assertThat(changeEvent.getPropertyField()).isEqualTo(fieldName);
|
||||
assertThat(changeEvent.getValue()).isEqualTo(newValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
void trackMultipleObjects() {
|
||||
EntityComparisonStrategy strategy = new EntityComparisonStrategy();
|
||||
Something something1 = new Something("oldValue");
|
||||
Something something2 = new Something("oldValue");
|
||||
|
||||
strategy.track(something1, description);
|
||||
strategy.track(something2, description);
|
||||
|
||||
String fieldName = "value";
|
||||
String newValue1 = "newValue1";
|
||||
String newValue2 = "newValue2";
|
||||
something1.value = newValue1;
|
||||
something2.value = newValue2;
|
||||
|
||||
Collection<EntityChangeEvent> changeEvents1 = strategy.getAggregatedEntityChangeEvents(something1);
|
||||
Collection<EntityChangeEvent> changeEvents2 = strategy.getAggregatedEntityChangeEvents(something2);
|
||||
|
||||
EntityChangeEvent changeEvent1 = changeEvents1.iterator().next();
|
||||
assertThat(changeEvent1.getPropertyField()).isEqualTo(fieldName);
|
||||
assertThat(changeEvent1.getValue()).isEqualTo(newValue1);
|
||||
|
||||
EntityChangeEvent changeEvent2 = changeEvents2.iterator().next();
|
||||
assertThat(changeEvent2.getPropertyField()).isEqualTo(fieldName);
|
||||
assertThat(changeEvent2.getValue()).isEqualTo(newValue2);
|
||||
}
|
||||
|
||||
class ParentClass {
|
||||
String parentValue;
|
||||
}
|
||||
|
||||
class Something extends ParentClass {
|
||||
final List<String> information = new ArrayList<>();
|
||||
String value;
|
||||
|
||||
Something(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
// create own equals and hashCode that should not get used in any technical parts of the dirty tracking
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Something something = (Something) o;
|
||||
return value.equals(something.value) && information.equals(something.information);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(value, information);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,14 +50,12 @@ import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.geo.Circle;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Metrics;
|
||||
import org.springframework.data.neo4j.core.NodeManagerFactory;
|
||||
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
|
||||
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
|
||||
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
|
||||
import org.springframework.data.neo4j.test.Neo4jExtension;
|
||||
import org.springframework.data.neo4j.test.Neo4jExtension.Neo4jConnectionSupport;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
@@ -830,25 +828,12 @@ class RepositoryIT {
|
||||
@Configuration
|
||||
@EnableNeo4jRepositories
|
||||
@EnableTransactionManagement
|
||||
static class Config {
|
||||
static class Config extends AbstractNeo4jConfig {
|
||||
|
||||
@Bean
|
||||
public static Driver driver() {
|
||||
public Driver driver() {
|
||||
|
||||
return neo4jConnectionSupport.openConnection();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NodeManagerFactory nodeManagerFactory(Driver driver) {
|
||||
|
||||
return new NodeManagerFactory(driver, PersonWithAllConstructor.class, PersonWithNoConstructor.class,
|
||||
PersonWithWither.class, ThingWithAssignedId.class, KotlinPerson.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PlatformTransactionManager transactionManager(Driver driver) {
|
||||
|
||||
return new Neo4jTransactionManager(driver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ import org.neo4j.driver.Values;
|
||||
import org.neo4j.driver.types.Point;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.neo4j.core.NodeManager;
|
||||
import org.springframework.data.neo4j.core.Neo4jClient;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
@@ -70,7 +70,7 @@ final class RepositoryQueryTest {
|
||||
private static final ProjectionFactory PROJECTION_FACTORY = new SpelAwareProxyProjectionFactory();
|
||||
|
||||
@Mock
|
||||
NodeManager nodeManager;
|
||||
Neo4jClient neo4jClient;
|
||||
|
||||
@Mock
|
||||
Neo4jMappingContext schema;
|
||||
@@ -97,7 +97,7 @@ final class RepositoryQueryTest {
|
||||
@Test
|
||||
void shouldSelectPartTreeNeo4jQuery() {
|
||||
|
||||
final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(NodeManager.class), mock(
|
||||
final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(Neo4jClient.class), mock(
|
||||
Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT);
|
||||
|
||||
RepositoryQuery query = lookupStrategy
|
||||
@@ -109,7 +109,7 @@ final class RepositoryQueryTest {
|
||||
@Test
|
||||
void shouldSelectStringBasedNeo4jQuery() {
|
||||
|
||||
final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(NodeManager.class), mock(
|
||||
final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(Neo4jClient.class), mock(
|
||||
Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT);
|
||||
|
||||
RepositoryQuery query = lookupStrategy
|
||||
@@ -125,7 +125,7 @@ final class RepositoryQueryTest {
|
||||
when(namedQueries.hasQuery(namedQueryName)).thenReturn(true);
|
||||
when(namedQueries.getQuery(namedQueryName)).thenReturn("MATCH (n) RETURN n");
|
||||
|
||||
final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(NodeManager.class), mock(
|
||||
final Neo4jQueryLookupStrategy lookupStrategy = new Neo4jQueryLookupStrategy(mock(Neo4jClient.class), mock(
|
||||
Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT);
|
||||
|
||||
RepositoryQuery query = lookupStrategy
|
||||
@@ -180,7 +180,7 @@ final class RepositoryQueryTest {
|
||||
|
||||
assertThatExceptionOfType(MappingException.class)
|
||||
.isThrownBy(
|
||||
() -> StringBasedNeo4jQuery.create(mock(NodeManager.class), mock(Neo4jMappingContext.class),
|
||||
() -> StringBasedNeo4jQuery.create(mock(Neo4jClient.class), mock(Neo4jMappingContext.class),
|
||||
QueryMethodEvaluationContextProvider.DEFAULT, method))
|
||||
.withMessage("Expected @Query annotation to have a value, but it did not.");
|
||||
}
|
||||
@@ -191,7 +191,7 @@ final class RepositoryQueryTest {
|
||||
Neo4jQueryMethod method = RepositoryQueryTest
|
||||
.neo4jQueryMethod("annotatedQueryWithValidTemplate", String.class, String.class);
|
||||
|
||||
StringBasedNeo4jQuery repositoryQuery = StringBasedNeo4jQuery.create(mock(NodeManager.class),
|
||||
StringBasedNeo4jQuery repositoryQuery = StringBasedNeo4jQuery.create(mock(Neo4jClient.class),
|
||||
mock(Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT,
|
||||
method);
|
||||
|
||||
@@ -210,7 +210,7 @@ final class RepositoryQueryTest {
|
||||
.neo4jQueryMethod("findByDontDoThisInRealLiveNamed", org.neo4j.driver.types.Point.class, String.class,
|
||||
String.class);
|
||||
|
||||
StringBasedNeo4jQuery repositoryQuery = StringBasedNeo4jQuery.create(mock(NodeManager.class),
|
||||
StringBasedNeo4jQuery repositoryQuery = StringBasedNeo4jQuery.create(mock(Neo4jClient.class),
|
||||
mock(Neo4jMappingContext.class), QueryMethodEvaluationContextProvider.DEFAULT,
|
||||
method);
|
||||
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.neo4j.repository.support;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.neo4j.core.NodeManager;
|
||||
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
|
||||
import org.springframework.data.neo4j.core.schema.Id;
|
||||
import org.springframework.data.neo4j.core.schema.IdDescription;
|
||||
import org.springframework.data.neo4j.core.schema.Node;
|
||||
import org.springframework.data.neo4j.core.schema.NodeDescription;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
class SimpleNeo4jRepositoryTest {
|
||||
|
||||
private NodeManager nodeManager;
|
||||
|
||||
private Neo4jMappingContext mappingContext;
|
||||
|
||||
private NodeDescription nodeDescription;
|
||||
|
||||
private SimpleNeo4jRepository<TestNode, Long> repository;
|
||||
|
||||
@BeforeEach
|
||||
void setupMock() {
|
||||
nodeManager = mock(NodeManager.class);
|
||||
|
||||
nodeDescription = mock(NodeDescription.class);
|
||||
when(nodeDescription.getPrimaryLabel()).thenReturn("TestNode");
|
||||
when(nodeDescription.getIdDescription()).thenReturn(new IdDescription());
|
||||
|
||||
mappingContext = mock(Neo4jMappingContext.class);
|
||||
when(mappingContext.getRequiredNodeDescription(TestNode.class)).thenReturn(nodeDescription);
|
||||
repository = new SimpleNeo4jRepository(this.nodeManager, this.mappingContext, TestNode.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveNotImplemented() {
|
||||
repository.save(null); // todo this should throw an exception upfront
|
||||
verify(nodeManager).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveAllNotImplemented() {
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.saveAll(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteByIdNotImplemented() {
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.deleteById(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteNotImplemented() {
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.delete(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteAll1NotImplemented() {
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class)
|
||||
.isThrownBy(() -> repository.deleteAll(Collections.emptyList()));
|
||||
}
|
||||
|
||||
@Node
|
||||
class TestNode {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
private String value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user