Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Session.load/queryForObject nullability #765

Merged
merged 6 commits into from
Feb 24, 2020
Merged

Fix Session.load/queryForObject nullability #765

merged 6 commits into from
Feb 24, 2020

Conversation

TWiStErRob
Copy link
Contributor

@TWiStErRob TWiStErRob commented Feb 20, 2020

Description

Add tests and fix nullability for these methods that can and will return null.

Related Issue

See first commit for failing tests for repro.

Motivation and Context

The mentioned method explicitly declare nullability, but they're restricted in the Kotlin extension functions.

/**
* Load single entity instance of type, with default depth = 1
*
* @return entity instance, null if not found
*/
<T, ID extends Serializable> T load(Class<T> type, ID id);

/**
* Load single entity instance of type, with depth
*
* @return entity instance, null if not found
*/
<T, ID extends Serializable> T load(Class<T> type, ID id, int depth);

* @return An instance of the objectType that matches the cypher and parameters. Null if no object
* is matched
* @throws java.lang.RuntimeException If more than one object is found.
*/
<T> T queryForObject(Class<T> objectType, String cypher, Map<String, ?> parameters);

This causes a problem when used like this:

import org.neo4j.ogm.session.queryForObject

class UserService(
	private val session: Session
) {

	fun find(id: String): User =
		session.queryForObject(
			"""
			MATCH (u:User{id:{id}})
			RETURN u AS user
			""",
			mapOf(
				"id" to id
			)
		)
}

Notice that because queryForObject declared a non-null return type, I can too. But when I call find with a wrong ID, it'll blow up with:

java.lang.IllegalStateException: queryForObject(T::class.java, cypher, parameters) must not be null

This means that a simple inline utility function changes behavior of the method on the interface.

Note that T is restricted to non-null (: Any) in generics. Adding T? as return value is explicitly making this nullable, which it should be. Alternatively changing to <T: Any?> would work too, but it's not safe as seen here:
image
notice how specifying the type argument for the function (explicit or inferred doesn't matter), we're telling Kotlin one thing, but receiving another. See how a non-null inferred result is actually passing an assertNull test?! 😨 If it was actually using that variable, it would blow up with NPE.

The big problem here is that in Kotlin a change like this is like changing from an interface to a concrete type in Java. It's going to break practically everyone who used this method. The fix is easy though: either !! if they're sure the value they're querying is there, or add a null check (if/?./?: fallback/?: return/?: error()) to signal appropriately for their project.

How Has This Been Tested?

Added unit tests.
TODO: should I add an integration test too for not found value being null?

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
    It is a breaking change because every time someone used this function they assumed non-null, see context above.

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
    As in: a big warning in Changelog and release notes.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

@michael-simons
Copy link
Collaborator

Good catch and bummer on my side :/ I'm gonna deal with the warning in the readme and changelog.

But I think it's much better to annoy people with a breaking change in the API in a patch release than having issues around nullability in Kotlin. WDYT, @meistermeier ?

@TWiStErRob
Copy link
Contributor Author

TWiStErRob commented Feb 21, 2020

I forgot to mention that SessionFactory.unwrap has the same problem, theoretically unwrap can be null:

public <T> T unwrap(Class<T> clazz) {
if (clazz == GraphDatabaseService.class) {
return (T) graphDatabaseService;
} else {
return super.unwrap(clazz);
}
}

if the driver is closed:
public synchronized void close() {
if (graphDatabaseService != null) {
logger.info("Shutting down Embedded driver {} ", graphDatabaseService);
graphDatabaseService.shutdown();
graphDatabaseService = null;
}
}

But I think that may need a different fix, maybe make sure EmbeddedDriver doesn't return null?
(BoltDriver will initialize itself, it'll never be null)
Shall I open a bug for this to figure out?

@michael-simons
Copy link
Collaborator

I think it's ok to change the unwrap extension to return possible null as well. I'll do this.

I'd rather see bolt and embedded driver behave the same I think: So either return null on the bolt driver when it's not initialised instead of initialising from that call. Gonna think about it…

@michael-simons michael-simons merged commit 1697956 into neo4j:master Feb 24, 2020
@TWiStErRob TWiStErRob deleted the twisterrob/extensions_nullability branch February 24, 2020 11:22
michael-simons added a commit that referenced this pull request Feb 24, 2020
…hods.

* Add failing tests for load/quaryForObject returning null
* Fix wrong signature (see called function's JavaDoc)
* Fix compilation errors after changing signature (this is expected for every user)
* Fix warnings in KotlinInteropTest.kt

Co-authored-by: Michael Simons <michael.simons@neo4j.com>
TWiStErRob added a commit to TWiStErRob/net.twisterrob.cinema that referenced this pull request Jul 18, 2021
[FIX] Add functions with correct nullability and use code that's like the reported neo4j/neo4j-ogm#765
TWiStErRob added a commit to TWiStErRob/net.twisterrob.cinema that referenced this pull request Aug 30, 2021
TWiStErRob added a commit to TWiStErRob/net.twisterrob.cinema that referenced this pull request Aug 30, 2021
* Update versions: Neo4J OGM 3.2.24 and Neo4J 4.2.0
* Remove now unnecessary hack: neo4j-ogm-core:3.2.24 depends on classgraph 4.8.86
* Fix tests by using an earlier version of Neo4jMatchers (3.4.9) and adapting it to latest.
* Migrate to Cypher 4 syntax for params
* Use explicit transaction name in beginTx().use {}
* Migrate CREATE UNIQUE to MERGE
* Introduce TimestampConverter to explicitly save with custom serialization
* Ignore unsafe reflective calls
* Fix logging of Neo4J OGM in tests
* Work around for duplicate ID in XML
* Work around neo4j/neo4j#12770 (SLF4J: Class path contains multiple SLF4J bindings.)
* Bump minor version to fix neo4j/neo4j#12655
* Fix: remove polyfills, neo4j/neo4j-ogm#765 is merged.
* Fix flaky test: separate JFixture objects would keep creating duplicate IDs at random. Sharing the instance will always create unique integers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants