spring-projects / spring-data-neo4j

Provide support to increase developer productivity in Java when using Neo4j. Uses familiar Spring concepts such as a template classes for core API usage and lightweight repository style data access.
http://spring.io/projects/spring-data-neo4j
Apache License 2.0
828 stars 620 forks source link

Add documentation for alternatives to the automatic index manager to the migration guide. #2105

Closed paolodedo closed 3 years ago

paolodedo commented 3 years ago

Hello,

Version 6.0.2 (spring boot 2.4.1) breaks the possibility of adding a "unique" constraint on a field, thanks to the annotation (present until spring boot 2.3.5.RELEASE) : @Index(unique = true)

Plus, there is no possibility of adding a "not null" constraint since the annotation @NonNull (import org.neo4j.driver.internal.shaded.reactor.util.annotation.NonNull) does nothing on validation.

Could you please help and explain?

Thanks!!

michael-simons commented 3 years ago

Sure. This is done on purpose. We recommend a tool like https://www.liquigraph.org or https://github.com/michael-simons/neo4j-migrations to create any constraints based on actual cypher scripts, not on a domain class.

We should however add this information to our migrating docs as well.

michael-simons commented 3 years ago

Btw, you should never use shaded, internal "things", in this case the NonNull annotation. This is not only not a SDN annotation, but comes via a transitive dependency from one of our dependencies.

paolodedo commented 3 years ago

Regarding @NonNull: I see your answer. In this logic, I can use (as I do for every SD impl) org.springframework.lang.NonNull annotation which...does not work either. Why? That is severe.

michael-simons commented 3 years ago

You can add this for sure, but for example JPA (with Spring Data JPA and any JPA implementation, i.e. Hibernate) will happily ignore it, too.

None of the validating annotations do influence the scheme.

If they don't validate your domain model, it is probably an issue with @Validated, but this is not an SDN concern.

paolodedo commented 3 years ago

Ok, I see.

I am trying the "IS NOT NULL" Cypher command but it is not working..

This is ok CREATE CONSTRAINT unique_deviceId IF NOT EXISTS ON (device:Device) ASSERT device.deviceId IS UNIQUE

This isn't ok CREATE CONSTRAINT notnull_deviceId IF NOT EXISTS ON (device:Device) ASSERT device.deviceId IS NOT NULL

michael-simons commented 3 years ago

Thank you, @paolodedo I am super happy your are trying this way.

So, first of all: The IF NOT EXISTS syntax is new in Neo4j 4.2. One more good reason for us not to bring that feature to SDN 6. The maintaince burden is rather high.

Regarding the not null constraint:

CREATE CONSTRAINT notnull_deviceId IF NOT EXISTS ON (device:Device) ASSERT EXISTS (device.deviceId);

Neo4j does not store null properties at all, so there fore it's about existence and the above index will assert that. This is a Enterprise edition only feature though.

paolodedo commented 3 years ago

Thank you! I appreciate a lot your help.

I am actually trying to get it working on a 4.2 Neo4j Enterprise Edt.

What happens is that Liquigraph returns:

2021-01-12 11:48:19.528  INFO 1404 --- [           main] o.l.core.io.lock.LiquigraphLock          : Failed to remove __LiquigraphLock. Trying again with new connection

java.sql.SQLException: java.sql.SQLException: com.fasterxml.jackson.databind.exc.MismatchedInputException: No content to map due to end-of-input
 at [Source: (org.apache.http.impl.io.EmptyInputStream); line: 1, column: 0]
    at org.neo4j.jdbc.http.driver.CypherExecutor.executeHttpRequest(CypherExecutor.java:421) ~[neo4j-jdbc-http-4.0.0.jar:na]
    at org.neo4j.jdbc.http.driver.CypherExecutor.executeQueries(CypherExecutor.java:223) ~[neo4j-jdbc-http-4.0.0.jar:na]
    at org.neo4j.jdbc.http.driver.CypherExecutor.executeQuery(CypherExecutor.java:236) ~[neo4j-jdbc-http-4.0.0.jar:na]
    at org.neo4j.jdbc.http.HttpNeo4jConnection.executeQuery(HttpNeo4jConnection.java:102) ~[neo4j-jdbc-http-4.0.0.jar:na]
    at org.neo4j.jdbc.http.HttpNeo4jPreparedStatement.execute(HttpNeo4jPreparedStatement.java:63) ~[neo4j-jdbc-http-4.0.0.jar:na]
    at com.zaxxer.hikari.pool.ProxyPreparedStatement.execute(ProxyPreparedStatement.java:44) ~[HikariCP-3.4.5.jar:na]
    at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.execute(HikariProxyPreparedStatement.java) ~[HikariCP-3.4.5.jar:na]
    at org.liquigraph.core.io.lock.LiquigraphLock.releaseLock(LiquigraphLock.java:151) ~[liquigraph-core-4.0.2.jar:na]
    at org.liquigraph.core.io.lock.LiquigraphLock.release(LiquigraphLock.java:64) ~[liquigraph-core-4.0.2.jar:na]
    at org.liquigraph.core.io.lock.LockableConnection.close(LockableConnection.java:102) ~[liquigraph-core-4.0.2.jar:na]
    at org.liquigraph.core.io.lock.LockableConnection.acquire(LockableConnection.java:77) ~[liquigraph-core-4.0.2.jar:na]
    at org.liquigraph.core.io.GraphJdbcConnector.connect(GraphJdbcConnector.java:44) ~[liquigraph-core-4.0.2.jar:na]
    at org.liquigraph.core.api.MigrationRunner$ConnectionSupplier.get(MigrationRunner.java:121) ~[liquigraph-core-4.0.2.jar:na]
    at org.liquigraph.core.api.MigrationRunner$ConnectionSupplier.get(MigrationRunner.java:111) ~[liquigraph-core-4.0.2.jar:na]
    at org.liquigraph.core.api.MigrationRunner.runMigrations(MigrationRunner.java:65) ~[liquigraph-core-4.0.2.jar:na]
    at org.liquigraph.core.api.Liquigraph.runMigrations(Liquigraph.java:101) ~[liquigraph-core-4.0.2.jar:na]
    at org.liquigraph.spring.SpringLiquigraph.afterPropertiesSet(SpringLiquigraph.java:60) ~[liquigraph-spring-boot-starter-4.0.2.jar:na]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1847) ~[spring-beans-5.3.2.jar:5.3.2]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1784) ~[spring-beans-5.3.2.jar:5.3.2]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:609) ~[spring-beans-5.3.2.jar:5.3.2]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:531) ~[spring-beans-5.3.2.jar:5.3.2]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.2.jar:5.3.2]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.2.jar:5.3.2]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.2.jar:5.3.2]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.2.jar:5.3.2]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944) ~[spring-beans-5.3.2.jar:5.3.2]
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:923) ~[spring-context-5.3.2.jar:5.3.2]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:588) ~[spring-context-5.3.2.jar:5.3.2]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:144) ~[spring-boot-2.4.1.jar:2.4.1]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:767) ~[spring-boot-2.4.1.jar:2.4.1]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759) ~[spring-boot-2.4.1.jar:2.4.1]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:426) ~[spring-boot-2.4.1.jar:2.4.1]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:326) ~[spring-boot-2.4.1.jar:2.4.1]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1309) ~[spring-boot-2.4.1.jar:2.4.1]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1298) ~[spring-boot-2.4.1.jar:2.4.1]

There are two things to mention: 1) some uniques clauses are already present due to a previous version of my application that used @Index(unique = true) 2) No write operations are allowed directly on this database. Writes must pass through the leader. The role of this server is: FOLLOWER -> can I apply these constraints on a FOLLOWER server? Or do I need to point to the LEADER one?

Thanks.

PS: I am okay with db-validation, but what if I'd want to backend-validate my data with Spring @NotNull annotation? Just to have data validated before they get to the db, in order to avoid much requests.

michael-simons commented 3 years ago

WRT to Liquigraph and migrations, @fbiville can you please chime in?

WRT to @NotNull, I am unsure. I use usally javax.validation.constraints.NotNull;. Find the following example:

public class Thing {

    @NotNull
    String title;

    String whathever;
}

(Getters and setters omitted)

the following controller

@RestController
public class ThingController {

    @PostMapping("/things")
    public String postThing(@Validated @RequestBody Thing thing) {
        System.out.println(thing);
        return "ok\n";
    }
}

and those dependencies

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>validation</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>validation</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

You'll see

curl -s -X POST -H "Content-Type:application/json" -d '{ "title" : "A title", "whathever": "Foobar" }' http://localhost:8080/things

succeeding and

curl -s -X POST -H "Content-Type:application/json" -d '{ "whathever": "Foobar" }' http://localhost:8080/things

failing with a bad request.

If you need to react to binding errors programmatically, here's one way:

    @PostMapping("/things")
    public String postThing(@Validated @RequestBody Thing thing, BindingResult bindingResult) {
        System.out.println(thing);
        return Boolean.toString(bindingResult.hasErrors());
    }
fbiville commented 3 years ago

@paolodedo for Liquigraph, can you please open an issue against this repository with the XML of the change sets and the version of Neo4j and Liquigraph you are using, as well as which Liquigraph client (Java core, Java Spring Boot, Maven plugin CLI)? I'll take a look there!

paolodedo commented 3 years ago

Ok thanks @fbiville , here it is.