Previously, we learned about CRUD operations, used to manage the objects (aka entities) in our data-set. In this section, we will talk about how to represent (and use) relations between these objects.
Relations are a natural concept in many domains: Friendships between users in a social network, tracks between stations in a subway system, the relation between a user and their orders in an e-commerce website, etc. In the RDBMS world, relations are stored implicitly, maintained using foreign keys and accessed via join queries. As your data-set grows, or the relations between its objects become more complex, join queries tend to get expensive (in terms of performance). The graph database world is very different - Relations are stored explicitly as edges, and the database is optimized for graph traversals (i.e. following edges).
We will define the relation from a user to their tweets. Remember, a single user may post many tweets, but each tweet is posted by one user. That is, we want to define a one-to-many relation between our user
and Tweet
types.
Let’s start by creating our Tweet
type.
Create the file lib/my_app/parts/tweet.rb
with the following code:
module MyApp
module Tweet
xn_part
property :text, type: :text
from_one :User
end
end
You will also need to edit lib/my_app/models.rb
, and add the following entry to the client_models
hash:
tweet: [Tweet]
A relation has two directions. From a Tweet
’s point of view, our relation is an incoming relation from a User
. From a User
’s point of view, our relation is an outgoing relation to a Tweet
.
In the code above, from_one :User
declares the from-direction (i.e. incoming) of the relation. We also need to define the to-direction.
We will do that by adding the following line to the User
module in lib/my_app/parts/user.rb
.
to_many :Tweet
Note: Related types are specified as symbols (e.g.
:Tweet
instead ofTweet
) to avoid circular dependencies between modules.
Important: You can reload code changes, in the console, with the command
MyApp.reload!
.
Although we haven’t created any tweets, our relation is defined, and we can access it using the API. We follow relations (i.e. get related objects) by appending to /rel/RELATION_NAME
to the path of a GET request.
For example, the following request will get all tweets of the user whose id is 73.
jruby-1.7.18 :017 > xget '/model/user/id/73/rel/tweets'
Total: 0
The relation name, tweets
, was generated automatically based on the related type (Tweet
). Since this is a many-relation, a plural inflection was applied to the name (i.e. tweets
instead of tweet
).
In order to make things a little more interesting, we should create a few tweets. Previously, we created objects using API calls. This time, we will create our tweets directly, using Ruby code.
Let’s create some tweets by running the following code in the IRB
tweets = ["It's a beautiful day", "Go #raptors!", "Learning #xnlogic is fun", "No more snow! #wintersucks"]
app.graph.tx do
users = app.all(MyApp::User).to_a
tweets.each do |text|
t = app.create(MyApp::M::Tweet, {text: text})
users.sample.add_tweets(t)
end
end
When interacting with the data using code (as opposed to the API), we have access to the underlying graph. For example, in the code above, we used
app.graph.v(MyApp::M::User)
to search the graph for all vertices of type user.
In this tutorial, we do not get into the details of the Ruby API, which is based on the open-source library Pacer.
For more information, see the Pacer docs.
Now that we have some data, let’s try the same query we tried before. We will ask for all the tweets of the user whose id is 73.
jruby-1.7.18 :018 > xget '/model/user/id/73/rel/tweets'
{:meta=>{:xnid=>"/model/tweet/id/81", :model_name=>"tweet", :rendered=>["record", "tweet"], :format=>"partial", :rel_limit=>50}, :id=>81, :name=>nil, :model_name=>"tweet", :description=>nil, :created_at=>2015-02-18 01:49:42 UTC, :text=>"No more snow! #wintersucks", :display_name=>81, :user=>{:meta=>{:xnid=>"/model/user/id/73", :model_name=>"user", :rendered=>["user", "record"], :format=>"compact"}, :id=>73, :name=>nil, :model_name=>"user", :display_name=>73}}
{:meta=>{:xnid=>"/model/tweet/id/82", :model_name=>"tweet", :rendered=>["record", "tweet"], :format=>"partial", :rel_limit=>50}, :id=>82, :name=>nil, :model_name=>"tweet", :description=>nil, :created_at=>2015-02-18 01:49:42 UTC, :text=>"Learning #xnlogic is fun", :display_name=>82, :user=>{:meta=>{:xnid=>"/model/user/id/73", :model_name=>"user", :rendered=>["user", "record"], :format=>"compact"}, :id=>73, :name=>nil, :model_name=>"user", :display_name=>73}}
Total: 2
=> #<Obj 1 ids -> lookup -> is_not(nil) -> V-Property(MyApp::M::User) -> V-Range(-1...1) -> outE(:tweet) -> inV -> V-Property(PacerModel::Extensions::M, PacerModel::Extensions::Record, MyApp::Tweet) -> V-Range(-1...100) -> Obj-Map>
We can also follow the relation in the opposite direction - Get a tweet by id, and follow its user
relation.
jruby-1.7.18 :019 > xget '/model/tweet/id/82/rel/user'
{:meta=>{:xnid=>"/model/user/id/73", :model_name=>"user", :rendered=>["record", "user"], :format=>"partial", :rel_limit=>50}, :id=>73, :name=>nil, :model_name=>"user", :description=>nil, :created_at=>2015-02-18 00:59:19 UTC, :username=>"cynthia", :email=>"sin.t.yeah@gmail.com", :display_name=>73}
Total: 1
=> #<Obj 1 ids -> lookup -> is_not(nil) -> V-Property(MyApp::M::Tweet) -> V-Range(-1...1) -> inE(:tweet) -> outV -> V-Property(PacerModel::Extensions::M, PacerModel::Extensions::Record, MyApp::User) -> V-Range(-1...1) -> Obj-Map>
Note: Since a tweet has a relation from one user, the relation name is in its singular form (i.e.
user
, and notusers
).
As you might expect, we can chain queries (aka traversals) together. For example, get the email of the user who posted tweet 82.
jruby-1.7.18 :020 > xget '/model/tweet/id/82/rel/user/properties/email'
[73, "sin.t.yeah@gmail.com"]
Total: 1
=> #<Obj 1 ids -> lookup -> is_not(nil) -> V-Property(MyApp::M::Tweet) -> V-Range(-1...1) -> inE(:tweet) -> outV -> V-Property(PacerModel::Extensions::M, PacerModel::Extensions::Record, MyApp::User) -> V-Range(-1...1) -> Obj-Map -> Obj-Range(-1...100)>
from_one
, from_many
, to_one
and to_many
keywords./rel/RELATION_NAME
to the path of the URL.