neo4j-contrib / neomodel

An Object Graph Mapper (OGM) for the Neo4j graph database.
https://neomodel.readthedocs.io
MIT License
936 stars 231 forks source link

Cant retrieve additional relations using `fetch_relations` #795

Closed diegopso closed 2 months ago

diegopso commented 3 months ago

Expected Behavior (Mandatory)

I expect that I can define models without defining StructuredRel and use the method fetch_relations to retrieve relationships of my model as described in https://neomodel.readthedocs.io/en/latest/getting_started.html#retrieving-additional-relations

Actual Behavior (Mandatory)

An exception is thrown:

neomodel.exceptions.RelationshipClassNotDefined: Relationship of type IS_FROM does not resolve to any of the known objects

How to Reproduce the Problem

Simple Example

For instance, the code below from docs (with adjusts) should work given the correct DB URL.

from neomodel import (
    IntegerProperty,
    RelationshipFrom,
    RelationshipTo,
    StringProperty,
    StructuredNode,
    UniqueIdProperty,
    config,
)

config.DATABASE_URL = "bolt://***"

class Country(StructuredNode):
    code = StringProperty(unique_index=True, required=True)
    inhabitant = RelationshipFrom("Person", "IS_FROM")

class City(StructuredNode):
    name = StringProperty(required=True)
    country = RelationshipTo(Country, "FROM_COUNTRY")

class Person(StructuredNode):
    uid = UniqueIdProperty()
    name = StringProperty(unique_index=True)
    age = IntegerProperty(index=True, default=0)

    # traverse outgoing IS_FROM relations, inflate to Country objects
    country = RelationshipTo(Country, "IS_FROM")

    # traverse outgoing LIVES_IN relations, inflate to City objects
    city = RelationshipTo(City, "LIVES_IN")

jim = Person(name="Jim", age=3).save()  # Create
jim.age = 4
jim.save()  # Update, (with validation)

germany = Country(code="DE").save()
jim.country.connect(germany)
berlin = City(name="Berlin").save()
berlin.country.connect(germany)
jim.city.connect(berlin)

results = Person.nodes.fetch_relations("country").all()
for result in results:
    print(result[0])  # Person
    print(result[1])  # associated Country

To make it work at least to the point of the issue I have, I added the inhabitant relation in country and changed the order of invocation of methods all and fetch_relations .

Work around

If I manually define the model of the relationship to be StructuredRel classes, the error is not triggered.

Adding:

from neomodel import StructuredRel

And changing the relation to:

inhabitant = RelationshipFrom("Person", "IS_FROM", model=StructuredRel)

Results in:

Screenshot 2024-05-13 153905

Although this works, it brings some weird trace info if looking in a debugger:

Screenshot 2024-05-13 153835

Screenshots (where it's possibile)

The error on my terminal:

Screenshot 2024-05-13 152946

Specifications (Mandatory)

Currently used versions

Versions

mariusconjeaud commented 3 months ago

@tonioo or @owinogradow, do you think you could have a look ? fetch_relations is something we merged back from the NN fork right ?

mariusconjeaud commented 3 months ago

@diegopso Coming back to this, I think that :

Note on id/element_id: we do provide the possibility to users to use id and element_id in whatever way they want, but we align with the Neo4j guideline, which is that those are internal ids that should only be used by the database itself; they are not guaranteed to be immutable. To identify your nodes in a robust way, you should apply your own unique identifier system.

mariusconjeaud commented 3 months ago

OK, I compared your example with the one in the documentation, and I can confirm that the problem is "just" that the documentation was very bad on that one. Sorry about that.

I will update accordingly, and before I do that, here is what is wrong/missing in the documentation:

Sorry for those two oversights; we had to integrate this back from an end-user branch, and it required quite some structural changes, and so we missed a few points (in that branch, which is actually a full application, relationships always have a model attached to them, so I missed that this was required)

mariusconjeaud commented 3 months ago

@diegopso Please check the linked PR. It does not change the behaviour (because it makes sense that neomodel expects a model to be defined for a relationship it is trying to resolve into a model object); but it fixes and improves the documentation, as well as the content of the exception, so it is clearer what is expected.

Thanks a ton for raising this up ! This is the latest big feature (except for async), and I feel we never truly completed it. That part definitely deserves more attention (it makes neomodel way more graphy after all)

diegopso commented 3 months ago

@mariusconjeaud thank you for the updates. I think now the docs look good.

I agree that a model for a relationship should be defined to be resolved. Still, when implementing a workaround on my side, I extended RelationshipTo and RelationshipFrom to change the default model argument in the init from None to StructuredRel. This, at least for me, made such that there is always a model to resolve the relationship to. This should be enough for all relationships that have no extra properties, and just custom relationships require writing specific classes.

I am mentioning it because you also have something similar for your passing fetch_relations tests in these classes: https://github.com/neo4j-contrib/neomodel/blob/795-cant-retrieve-additional-relations-using-fetch_relations/test/sync_/test_match_api.py#L35-L46

Used in this test case: https://github.com/neo4j-contrib/neomodel/blob/795-cant-retrieve-additional-relations-using-fetch_relations/test/sync_/test_match_api.py#L531

Still, as is now is much easier to understand and use it.

mariusconjeaud commented 3 months ago

Your solution is interesting. I actually thought about that while working on this, but I was looking at a different place where to add StructuredRel as default, and it was not suitable. But your solution looks better ! It would need testing though, to make sure it's not filling the node class registry with many relationship classes.

I mean that if you define two distinct relationships with the same type string (like, IS_FROM), then it might be trying to create two entries with that key in the registry and trigger a conflict.