Relations I

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).

Our first relation

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 of Tweet) to avoid circular dependencies between modules.

Important: You can reload code changes, in the console, with the command MyApp.reload!.

API syntax

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).

Create Tweets

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.

Follow relations

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 not users).

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)>

Summary

  • Relations are defined using the from_one, from_many, to_one and to_many keywords.
  • To-relation and from-relations are matched and named automatically, based on the related types.
  • Plural inflection is automatically applied to the names of many-relations.
  • To access a relation using the API, we append /rel/RELATION_NAME to the path of the URL.