Schema
General
Entities
A graph is composed of two entities: node and relationship, each with its own specifities.
Node has:
- one or more labels
- 0 or more properties
Relationship has:
- one type
- 0 or more properties
- a direction
About Neo4j internal ids
Neo4j uses id to manage its own internal ids, therefore the field :id is forbidden
for node node or relationship properties.
This particular information can be found in the Node and Realtionship struct at the key :__id__.
About naming conventions
Seraph enforces naming best practices recommended by Neo4j:
- Node labels should be CamelCased, starting with a uppercased character (ex: UserProfile)
- Relationship should be UPPERCASED (ex: WROTE)
- properties should be camelCased, starting with a lowercased character (ex: firstName)
Non respect of these rules will raise errors.
GraphApp schemas
In our project, we need 4 nodes schema: User, UserProfile, Post and Comment. Let's start with the User.
A simple node: User
Its properties are:
- firstName
- lastName
- emailTo define them is as simple as this:
# lib/graph_app/blog/user.ex
defmodule GraphApp.Blog.User do
use Seraph.Schema.Node
node "User" do
property :firstName, :string
property :lastName, :string
property :email, :string
end
endUser identifier is defined by default to :uuid and is an Ecto.UUID.
We also have merge keys, which will be used to get the desired node in set
and merge operations.
We can also add a changeset/2 function to validate our data:
# lib lib/graph_app/blog/user.ex
...
import Seraph.Changeset
...
def changeset(%User{} = user, params \\ %{}) do
user
|> cast(params, [:firstName, :lastName, :email])
|> validate_required([:firstName, :lastName, :email])
endSeraph.Changeset is a subset of Ecto.Changeset and most of the usual functions
are available.
Now, our node has outgoing and incoming relationships and we would like to define them.
Our first relationship
Let's work on the first one: (User)-[:WROTE]->(Post) which is a 1-x relationship.
It has one property: when which is a datetime.
Relationships have their own schema and the one for :WROTE is as this:
# lib/graph_app/blog/relationship/wrote.ex
defmodule GraphApp.Blog.Relationship.Wrote do
use Seraph.Schema.Relationship
@cardinality [outgoing: :one, incoming: :many]
relationship "WROTE" do
start_node GraphApp.Blog.User
end_node GraphApp.Blog.Post
property :when, :utc_datetime
end
endA changeset/2 would be nice too:
# lib/graph_app/blog/relationship/wrote.ex
...
import Seraph.Changeset
...
def changeset(%Wrote{} = wrote, params \\ %{}) do
wrote
|> cast(params, [:start_node, :end_node, :when])
|> validate_required([:when])
endNote that :start_node and :end_node have to to be set in casted params.
This allows to have changeset when only one of them or neither can be changed.
Useful if you want to have a reelationship with a fixed start/end.
Back to User schema
Now adding the relationship to User node schema:
# lib/graph_app/blog/user.ex
...
alias GraphApp.Blog.Relationship
...
node "User" do
outgoing_relationship "WROTE", GraphApp.Blog.Post, :posts, Wrote, cardinality: :many
...WROTE is the relationship type
GraphApp.Blog.Post is the end node linked by the relationship
:posts is the struct field where preloaded (Post) nodes would be found
Wrote is the realtoinship module
cardinality: :many defines the cardinality of the relationship
Not that type, end node module and cardinality must match the ones defined in the relationship module. In fact, they are listed here to understand what the node is quickly.
A field :wrote will be available in the struct to hold the preloaded relationships
Relationships without properties: define them quickly
In a Neo4j database model, there could be a significant amount of relationship without properties. Having to add a module for each of them could be very tedious. To avoid this, Seraph offers a quic k way to define them via defrelationship.
Let's do it for our four remaining relationships:
- (User)-[:WROTE]->(Comment) (1 - x)
- (User)-[:READ]->(Post) (x - x)
- (User)-[:FOLLOWS]->(User) (1 - x)
- (User)-[:HAS_PROFILE]->(UserProfile) (1 - 1)#lib/graph_app/blog/relationship/no_properties.ex
defmodule GraphApp.Blog.Relationship.NoProperties do
import Seraph.Schema.Relationship
alias GraphApp.Blog.{User, Post, Comment, UserProfile}
defrelationship "WROTE", User, Comment, cardinality: [incoming: :one]
defrelationship "READ", User, Post
defrelationship "FOLLOWS", User, User, cardinality: [incoming: :one]
defrelationship "HAS_PROFILE", User, UserProfile, cardinality: [outgoing: :one, incoming: :one]
defrelationship("IS_ABOUT", Comment, Post, cardinality: [outgoing: :one, incoming: :many])
endwill expand to:
defmodule GraphApp.Blog.Relationship.NoProperties do
defmodule UserToComment.Wrote do
use Seraph.Schema.Relationship
@cardinality [incoming: :one]
relationship "WROTE" do
start_node User
end_node Comment
end
end
defmodule UserToPost.Read do
use Seraph.Schema.Relationship
relationship "READ" do
start_node User
end_node Post
end
end
defmodule UserToUser.Follows do
use Seraph.Schema.Relationship
@cardinality [incoming: :one]
relationship "FOLLOWS" do
start_node User
end_node User
end
end
defmodule UserToUserProfile.HasProfile do
use Seraph.Schema.Relationship
@cardinality [outgoing: :one, incoming: :one]
relationship "HAS_PROFILE" do
start_node User
end_node UserProfile
end
end
defmodule CommentToPost.IsAbout do
use Seraph.Schema.Relationship
@cardinality [outgoing: :one, incoming: :many]
relationship "IS_ABOUT" do
start_node Comment
end_node Post
end
end
endComplete User node schema
Here is our final User schema with all its relationships:
# lib/graph_app/blog/user.ex
defmodule GraphApp.Blog.User do
use Seraph.Schema.Node
import Seraph.Changeset
alias GraphApp.Blog.User
alias GraphApp.Blog.Relationship
alias GraphApp.Blog.Relationship.NoProperties
node "User" do
property :firstName, :string
property :lastName, :string
property :email, :string
outgoing_relationship("WROTE", GraphApp.Blog.Post, :posts, Relationship.Wrote,
cardinality: :many
)
outgoing_relationship(
"WROTE",
GraphApp.Blog.Comment,
:comments,
NoProperties.UserToComment.Wrote,
cardinality: :many
)
outgoing_relationship("READ", GraphApp.Blog.Post, :read_posts, NoProperties.UserToPost.Read,
cardinality: :many
)
outgoing_relationship(
"HAS_PROFILE",
GraphApp.Blog.UserProfile,
:profile,
NoProperties.UserToUserProfile.HasProfile,
cardinality: :one
)
outgoing_relationship(
"FOLLOWS",
GraphApp.Blog.User,
:followed,
NoProperties.UserToUser.Follows,
cardinality: :many
)
end
def changeset(%User{} = user, params \\ %{}) do
user
|> cast(params, [:firstName, :lastName, :email])
|> validate_required([:firstName, :lastName, :email])
end
endWe have 2 :WROTE relationships here... But it is not a problem, every :WROTE relationships will be preloaded in the :wrote field.
Your usage / need for documentation defines your schema
It is not mandatory to define all the relationships of a particular node.
For example, this is valid:
defmodule GraphApp.Blog.UserProfile do
use Seraph.Schema.Node
import Seraph.Changeset
alias GraphApp.Blog.UserProfile
node "UserProfile" do
property :isPremium, :boolean
property :age, :integer
end
def changeset(%UserProfile{} = user_profile, params \\ %{}) do
user_profile
|> cast(params, [:isPremium, :age])
|> validate_required([:isPremium, :age])
end
endWe haven't define the (User)-[:HAS_PROFILE]->(UserProfile) as we've done in User.
This is your choice:
- not having it -> Make it impossible to go from UserProfile to User
- having it -> Complete documentation of database schema
The other node schemas
Post
defmodule GraphApp.Blog.Post do
use Seraph.Schema.Node
alias GraphApp.Blog.{Comment, User}
alias GraphApp.Blog.Relationship.{NoProperties, Wrote}
node "Post" do
property :title, :string
property :text, :string
property :rate, :integer, default: 0
incoming_relationship("WROTE", User, :author, Wrote, cardinality: :one)
incoming_relationship("READ", User, :readers, NoProperties.UserToPost.Read, cardinality: :many)
incoming_relationship("IS_ABOUT", Comment, :comments, NoProperties.CommentToPost.IsAbout,
cardinality: :many
)
end
endComment
defmodule GraphApp.Blog.Comment do
use Seraph.Schema.Node
alias GraphApp.Blog.{Post, User}
alias GraphApp.Blog.Relationship.NoProperties
node "Comment" do
property :text, :string
outgoing_relationship("IS_ABOUT", Post, :post, NoProperties.CommentToPost.IsAbout,
cardinality: :one
)
incoming_relationship("WROTE", User, :author, NoProperties.UserToComment.Wrote,
cardinality: :one
)
end
end