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
- email
To 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
end
User 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])
end
Seraph.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
end
A 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])
end
Note 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])
end
will 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
end
Complete 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
end
We 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
end
We 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
end
Comment
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