Modeling
As this library is based on GraphQL Kotlin and Spring Data Neo4j, the modeling is mostly a merge of their approaches. Almost all of their features still work when using this library. For more information on their features, please have a look at GraphQL Kotlin and Spring Data Neo4j
Overview
Consider a simple domain model, consisting of movies, actors and directors:
Each movie has exactly one director, and any amount of actors.
Given is the code for the Movie
node:
@DomainNode("movies") // (1)
@Authorization("READ") // (2)
class Movie(
@OrderProperty @FilterProperty val name: String, // (3)
internal val releaseYear: Int // (4)
) : Node() { // (5)
@NodeRelationship("DIRECTOR", Direction.OUTGOING) // (6)
val director by NodeProperty<Director>()
@NodeRelationship("ACTOR", Direction.OUTGOING) // (7)
val actors by NodeSetProperty<Actor>()
fun actorsCount() = actors.size // (8)
}
- Necessary annotation for all
Node
classes. If thename
parameter is provided, a connection-like query with the namename
is exposed. - Specifies the
READ
permission for this Node, for more info, see Authorization - Property which is exposed in the GraphQL schema. As it is annotated with both
@OrderProperty
and@FilterProperty
, connections of typeMovie
can both be filtered and ordered by this property. - Internal property NOT exposed in the GraphQL schema.
- Each class needs to extend
Node
in order to use any GraphGlue specific features. - An outgoing relation of type
Director
, each movie has exactly one director. - An outgoing relation of type
Set<Actor>
. In the GraphQL schema, it is exposed asActorConnection
- Function exposed as field in the GraphQL schema.
Class Definition
Each node class both has to be annotated with @DomainNode
and extend io.github.graphglue.model.Node
.
If any of those conditions is missing, GraphGlue-specific features might not work.
This does not mean that you never should use classes which fullfill neither of those criteries.
For example, a non-node class can be used as type for properties with a struct-like type: it is still persisted in the database as node, however, in the GraphQL schema, it does not implement Node
, meaning it is not possible to retrieve it via the node
query.
The Node class also defines an id. In Kotlin, it is possible to access the id using the rawId
String property.
It is not possible to manually assign an id, as a id is automatically generated when the node is first saved.
However, the generation can be customized by providing a bean with the name io.github.graphglue.model.NODE_ID_GENERATOR_BEAN
and the type IdGenerator<String>
.
By default, a random UUID is generated.
Properties
Properties are both used to expose fields in the GraphQL schema, and save properties on the node in the database.
By default, both Spring Data Neo4j and GraphQL Kotlin use the name of the property as name, however, the name in the GraphQL schema can be changed by annotating it with @GraphQLName("name")
, the name of the property in the database can be changed by annotating it with @Property("name")
.
Spring Data Neo4j and GraphQL Kotlin use different visibilities.
While properties backed by a field (this includes delegated properties) are saved in the database, GraphQL Kotlin only exposes public
properties not annotated with @GraphQLIgnore
.
GraphGlue automatically adds all injected GraphQLType
s to the generated schema.
This allows for injecting custom scalars, and then using the name in @GraphQLType
annotations.
The used type must both be supported by GraphQL Kotlin and Spring Data Neo4j, however, both libaries provide extension mechanisms which allow you to support additional types.
Functions
Like properties, functions can be used to expose field in the GraphQL schema.
Relationships
GraphGlue supports two types of relationships: native GraphGlue relationships, and Spring Data Neo4j relationships. Both support many-to-many, many-to-one and one-to-one relationships by combining one- and many-sides of relationships. Both bidirectional (defining both ends) and unidirectional (defining only one end) relationships are supported. Note that for bidirectional relationships, both sides must use the same type of relationship, it is not possible to combine GraphGlue with Spring Data Neo4j relationship sides.
GraphGlue relationships
Declaration of one-sides of relationships
@NodeRelationship(label, direction)
val propertyName by NodeProperty<NodeType>()
Declaration of many-sides of relationships
@NodeRelationship(label, direction)
val propertyName by NodeSetProperty<NodeType>()
where label
is a String
, and direction is either Direction.INCOMING
or Direction.OUTGOING
.
Relations are always directional, due to being directional in Neo4j.
When modeling a bidirectional relationship, one side needs to use Direction.INCOMING
, while the other needs to use Direction.OUTGOING
.
The direction is considered an implementation detail and not exposed in the GraphQL API.
To actually get the content of a property, you have to use the call operator on the property (e.g. node.manyProperty().add(addedNode)
).
This is necessary, as lazy loading is done asynchronous, and properties cannot be marked currently with suspend
.
Therefore, on invoking, the property is - if necessary - loaded from the database.
On save, all relationships are saved. Save cascades down added entities, but removed ones.
Example: If the one side has initially the value node1
, which then is replaced with node2
, when saving, node2
is saved (as it was "added"), while node1
is not.
Spring Data Neo4j relationships
Declaration of one-sides of relationships
@Relationship(label, direction)
val propertyName: NodeType
Declaration of many-sides of relationships
@Relationship(label, direction)
val propertyName: List<NodeType>
For more information, see Connecting nodes: @Relationship
Comparison
GraphGlue | Spring Data Neo4j | |
---|---|---|
GraphQL representation | One sides are represented by their appropriate type. Many sides are represented by connection types, supporting pagination, filtering and ordering | One sides are represented by their appropriate type. Many sides are represented as GraphQL list, without pagination, filtering or ordering support |
Lazy loading | Only lazy loading is supported. Relations are automatically loaded when accessed. Note: when fetching data for GraphQL, the whole subtree is loaded at once using one Cypher query, preventing the n+1 problem. Lazy loading is done asynchronous. | No lazy loading is supported, all relationships are eagerly loaded, which can result in large subgraphs being loaded. To prevent this, you may use Projections |
Matching of opposite sides | Opposite sides are matched if the label is the same, but the direction the opposite. |
Inheritance
Currently, only inheriting from abstract classes is supported.
Specifically, inheriting from open
, non-abstract
classes is only supported if it is marked with @GraphQLIgnore
.
Inheriting from interfaces is possible, however none of the GraphGlue annotations (@DomainNode
, @Authorization
, @AdditionalFilter
, @AdditionalOrder
) is supported there, however, it can e.g. be used to share common @GraphQLDescriptions
.
The @DomainNode
annotation is not inherited and has to be present on all node types.
Ordering & Filtering
For more information on how to use ordering and filtering in GraphQL, see Connections
Ordering
All nodes can be ordered by id.
Additionally, nodes of a specific type can also be ordered by all properties annotated with @OrderProperty
.
As those might not be unique, the id
is used to create a strict total order.
By default, all data types which are comparable in Cypher are supported.
Furthermore, NodeProperty<*>
backed properties allow ordering by all orderable properties of the related node type, excluding NodeProperty<*>
backed properties.
Ordering on NodeProperty<*>
does NOT take the authorization system into account.
By making such a property orderable, it is possible to leak information about non-visible nodes.
Only use when access to the related node is automatically granted!
Additional orders
With the AdditionalOrder("beanName")
annotation, property independent orders can be defined.
This can for instance be used to order by a complex condition.
A Spring bean with the specified name and type OrderPart
has to be provided.
Filtering
Filters are generated for specific node types. These filters are generated by concatenating property-based filter fields and additional filter fields. Then, for each type, a meta filter is created, which allows joining filters by and, or and not:
input TypeNodeFilterInput {
# filter fields
}
input TypeFilterInput {
and: [TypeFilterInput!]
or: [TypeFilterInput!]
not: TypeFilterInput
node: TypeNodeFilterInput
}
Exactly one of the specified fields has to be provided.
Filters for properties
Properties can be annotated with @FilterProperty
to allow filtering by a specific property. By default, this is supported for properties with the following types:
String
Int
Double
(Float in GraphQL)Boolean
ID
by NodeProperty<*>
by NodeSetProperty<*>
Input fields of filters are always optional. All the present fields are then joined by &&
.
The input fields of the filter depends on the type: For Boolean
and ID
, only eq
and in
are available.
For all comparable types (Int
, Double
and String
), lt
, lte
, gt
and gte
are additionally available.
Finally, for String
, startsWith
, endsWith
, contains
and matches
are also available.
For NodeProperty<*>
backed properties, the meta filter for the specific type is used.
For NodeSetProperty<*>
backed properties, the filter fields include all
, some
and none
, each can be set to a type specific meta filter.
It is possible to support additional property types, by providing Spring beans of type TypeFilterDefinitionEntry
.
Here an example for the Kotlin Float
type:
@Bean
fun floatFilter() =
TypeFilterDefinitionEntry(Float::class.createType()) { name, property, parentNodeDefinition, _ ->
FloatFilterDefinition(
name, parentNodeDefinition.getNeo4jNameOfProperty(property), property.returnType.isMarkedNullable
)
}
Note that TypeFilterDefinitionEntry
are free to not create a filter definition by returning null
in the callback.
This is e.g. be used for filters for Node(Set)Property<*>
if the property has a generic type.
Filter on NodeProperty<*>
and NodeSetProperty<*>
take the authorization system into account.
Conceptually, first the related nodes are filtered by the Permission
to only include nodes the Permission grants access to, then,
the filter is evaluated.
This prevents an information leak by filtering, however, at the cost of higher complexity.
Therefore, use @FilterProperty
on properties which may include non-visible nodes with caution.
Additional filters
With the AdditionalFilter("beanName")
annotation, property independent filters can be defined.
This can for instance be used to filter by Node type, or a complex condition.
A Spring bean with the specified name and type FilterEntryDefinition
has to be provided.
Node filter generators
Bean of type NodeFilterGenerator
can be used to generate additional filter entries for any Node filters.
In contrast to Additional filters, node filter generators allow for dynamically generating the entries
based on the provided NodeDefinition
, which allows e.g. implementing custom meta-filters (like xor).