Data Modeling With XN

Brought to you by the good folks at

Introduction to

Traditional Data Modeling

  • Object-oriented.
  • Class-hierarchy tree.
  • Interfaces and abstract classes.
  • Flexible and powerful model.
    • Abstraction
    • Encapsulation
    • Inheritance
    • Polymorphism
  • The "bread and butter" of any software engineer.

Traditional Data Modeling

  • Any problems with the traditional model?
    • Single inheritance is limiting.
    • Multiple inheritance has its own issues
    • Some mistakes are too easy to make.
    • Not necessarily the best fit for every domain.

The XN model

  • Parts
    • Ruby modules.
    • Contain getters, setters and other methods.
    • Also known as extensions.
  • Models
    • Defined as a list of parts.
    • Do not contain any functionality.
    • You can think of models as types.
  • We establish "has a" relation between a model and its parts, as opposed to "is a" relation between a class and its parent.

The XN model + Graph DB

  • Graph DB
    • Flexible general-purpose structure.
    • Vertices & edges, both with (optional) properties.
    • Vertices = entities, edges = relations.
  • Parts
    • Extend vertices with functionality.
    • Vertices store data, parts define methods.
  • Models
    • Types, defined as a list of parts.
    • Each vertex belongs to one model, and can be extended by any part in its model's list.

More On The XN model

  • A part can belong to multiple models.
  • A model can have multiple parts.
  • Can create/delete model object (i.e. entities), but not parts.
  • Can read/view part(s) of a model object.

Let's Get Started

  • Please make sure you have
    • Installed the xnlogic command-line tool,
    • Created an application,
    • And logged into the VM.

CRUD operations

The basics

Our Data Model

  • Two types of objects:
    • User
      • username
      • email
    • Tweet
      • text
  • In this simple example, the model is the part.

Create lib/my_app/parts/user.rb

module MyApp
  module User
    xn_part

    property :username, type: :text
    property :email, type: :text
  end
end

Edit lib/my_app/models.rb


  client_models = {
     #...
     user: [User]
     #...
  }

Define the User part

Define a user model, composed of a single part, User.

In the host machine, using your favorite IDE

> xput '/model/user', {username: 'cynthia', email: 'sin.t.yeah@gmail.com'}

Load the application, and get ready to simulate API calls from the console.

> app = PM['db01']
> include app.console

In the VM, test API using the xn-console

> xget '/model/user'
> xpatch '/model/user/id/71', {email: 'john@doe.com'}
 > xdelete '/model/user/id/71'

Create - PUT

Read - GET

Update - PATCH

Delete - DELETE

GET /model/user

More API Calls

GET /model/user/id/56
GET /model/user/properties
GET /model/user/id/42/properties/PROP_NAME1,PROP_NAME2

Get all objects of type foo.

Get a foo object by id.

Get the names of all properties defined in type foo.

Get values of specific properties

GET /model/user/first
GET /model/user?limit=15
GET /model/user?offset=15&limit=15

Limit the number of results.

Relations

Connecting entities...

Edit lib/my_app/parts/user.rb

module MyApp
  module User
    xn_part

    property :username, type: :text
    property :email, type: :text

    to_many :Tweet
  end
end

Create lib/my_app/parts/tweet.rb

module MyApp
  module Tweet
    xn_part

    property :text, type: :text

    from_one :User

  end
end

Relating Users and Tweets

    to_many :Tweet
GET /model/user/id/73/rel/tweets
GET /model/tweet/id/82/rel/user/properties/email

The API

Get related objects

You may want to create some data first.

What about CRUD operations for relations?

Not necessary. Update entities instead.

Relations, so far ...

  • We've seen one-to-many.
    Other types can be defined using the from_one, from_many, to_one and to_many keywords.
  • From and to relations are matched by type.
  • Automatic plural inflection for many relations.
  • API access by appending /rel/RELATION_NAME.

Edit lib/my_app/parts/user.rb

module MyApp
  module User
    xn_part

    property :username, type: :text
    property :email, type: :text

    to_many :Tweet
    to_many   :User, to: :follows
    from_many :User, to: :follows, from: :followed_by

  end
end

Naming Relations

The to attribute

GET /model/user/id/73/rel/follows
GET /model/user/id/73/rel/followed_by

Access the relation using the API

Tip: In the xn-console, don't forget to                          the changes.

MyApp.reload!
    to_many   :User, to: :follows
    from_many :User, to: :follows, from: :followed_by
  1. Defines the name of a to-relation.
  2. Matches from-relations with to-relations.

Actions

Modifying relations

Introducing Actions

  • Instance methods,
  • Available through the API.
  • Use actions to create, update or delete relations.

In our example, creating a tweet will be a user's action:

  • Create the tweet.
  • Create the relation to/from its author.

Add the following to the User module in lib/my_app/parts/user.rb

module Vertex

  def post_tweet(t)
    tweet = self.app.create(M::Tweet, {text: t})
    tweet.user = self
    tweet
  end

end
action :post_tweet, 
  args: [{:t => :text}] do |context, t|
    self.post_tweet(t)
  end

Define the logic:

  • Inside a Vertex module.
  • Note: Returning the Tweet is optional.

Defining Our Action

Expose the action to the API:

  • Name & arguments.
  • Block of code.
  • Good practice: Logic is defined in an instance method.

At this point, lib/my_app/parts/user.rb should look this:

module MyApp
    module User
        xn_part

        property :username, type: :text
        property :email, type: :text

        to_many :Tweet
        to_many   :User, to: :follows
        from_many :User, to: :follows, from: :followed_by

        action :post_tweet, args: [{:t => :text}] do |context, t|
            self.post_tweet(t)
        end

        module Vertex
            def post_tweet(t)
                tweet = self.app.create(M::Tweet, {text: t})
                tweet.user = self
                tweet
            end
        end    

    end
end

Full Source Code

> xpost '/model/user/id/73/action/post_tweet', {t: "Hello World"}

Perform Action Using The API

> xget '/model/user/id/73/rel/tweets/properties/text'
> xget '/model/tweet/id/85/rel/user/properties/username'

Perform an action by send a POST request with JSON data.

Verify that we can get the tweet from the user.

Verify that we can get the user from tweet.

As before, we will simulate API calls using the xn-console.

Summarizing Actions

  • Instance methods that are exposed to the API.
  • Access via a POST request.
    • Append /action/ACTION_NAME to the URL.
    • Send data as JSON object.
  • Defining an action involves:
    • Declaring the API interface.
    • Defining the logic.
  • Good practices: Define the logic in an instance method (i.e. inside the Vertex module).
  • Actions are automatically wrapped in a transaction.
  • See additional examples of defining actions.

Custom Traversals

Extend the API with

The Problem

GET model/user/id/73/rel/followed_by/rel/followed_by/rel/tweets

So far, we have seen the most basic API queries:

  • Getting entities by type.
  • Following an entity's relation.

As queries become more complex (and interesting), URLs can become very long.

The Solution

GET model/user/id/73/to/suggested_tweets

In other words, turn something like

GET model/user/id/73/rel/follows/rel/followed_by/unique/rel/tweets/...

Defining traversals, similar to actions, involves:

  1. Defining the logic.

     
  2. Exposing to the method to the API.
  • Using Ruby.
  • With the full power of Pacer.

Extend the API with custom traversals.

Into

Add the following to the User module in lib/my_app/parts/user.rb

route_traversal :similar_users, 
    return_parts: :User do
        self.similar_users.uniq
    end
module Route

    def similar_users()
        self.as(:user).follows.followed_by.is_not(:user)
    end

end
  • Name
  • Return type
  • Block of code (self is a route).

Defining Our Traversals

  • Inside a Route module.
  • self is a route, think of it as a composable stream of vertices.

Define (most of) the logic.

Declare the API method.

GET model/user/id/73/to/similar_users

The API

GET model/user/id/73/to/similar_users/properties/username
GET model/user/id/73/to/similar_users/rel/tweets/properties/text

Use our custom traversal

Chain it with other queries

Real World Example

Recommend new users to follow.

  • Look at users who follow the same people as me.
  • See who else they are following.

Let's declare the API action

route_traversal :should_follow, return_parts: :User do
    self.recommended_users_to_follow(10)
end

We'll create the recommend_users_to_follow method in the next slide.

This will allow us to make the following API calls:

GET model/user/id/73/to/should_follow
GET model/user/id/73/to/should_follow/rel/follows/unique/rel/tweets/property/text

Add the following inside Route module of the User part.

def recommended_users_to_follow(limit)
    self.similar_users.follows
        .except(self).except(self.follows)
        .most_frequent(0...limit)
end

Implementing The Query

  1. Use the similar_users method and follows relation.
  2. Exclude myself, and users I am already following, from the result.
  3. Include only the most frequent users.

Since the similar_users method doesn't remove duplicates.

  • ​Users who are more similar to me occur more times in self.simlar_users.
  • And people that they follow occur more times in self.simlar_users.follows.

Custom Traversals - Summary

  • Define custom traversals in the Route module.
  • Expose traversals to the API.
  • Extend the API with your domain-specific language.
  • Construct URLs using /to/traversal_name.
  • Chain traversals.

A Few More Data Modeling Features ...

Edit lib/my_app/parts/user.rb

module MyApp
  module User
    xn_part

    property :username, type: :text
    property :email, type: :text

    # ...

Filtering/Searching

GET model/user/filter/username?username=lenora

Search for a user whose username is lenora

, filter: true

Enable filter/search by the username property.

GET model/user/filter/username?username[regex]=.*x.*

Using regular expressions, search for all users whose username contains the letter x.

GET model/user/filter/username/rel/follows/filter/username~1/count?username=lenora&username~1=loren

Does lenora follow loren?

GET model/user/filter/username/rel/follows?username=lenora

Who does lenora follow?

Edit lib/my_app/parts/user.rb

module MyApp
  module User
    xn_part

    property :username, type: :text
    property :email, type: :text




    # ...

Display Properties

    display :badge do
        self.followed_by.count > 10 ? "Popular" : "--"
    end

Display properties are

  • Computed on demand.
  • Simpler getter methods.
GET model/user/properties/username,badge

Using the API, we can get display properties like any other property.

Edit the User module in lib/my_app/parts/user.rb

    action :post_tweet, args: [{:t => :text}],                           do |context, t|
        self.post_tweet(t)
    end

Guarding Actions

        def guard_post_tweet(errors, context, t)
            if t.length > 140
                errors.add(:t, "Tweet is too long.")
            end
        end
> xpost '/model/user/id/73/action/post_tweet', {t: "X" * 140}

> xpost '/model/user/id/73/action/post_tweet', {t: "X" * 141}
 => {:response=>:validation_errors, ... , :errors=>{"t"=>["Tweet is too long."]}} 

Test API calls in the console

    module Vertex







        #...
, guard: :guard_post_tweet

Edit the User module in lib/my_app/parts/user.rb

    document :stats do |context|
        self.statistics_report()
    end

Documents

        def statistics_report
            {'follows_count'     => self.follows.count, 
             'followed_by_count' => self.followed_by.count}
        end
> xget '/model/user/id/73/document/stats'
 => {"follows_count"=>14, "followed_by_count"=>12}

Test API calls in the console

    module Vertex







        #...

Source Code

The full source code in lib/my_app/parts/user.rb

module MyApp
  module User
    xn_part

    property :username, type: :text, filter: true
    property :email, type: :text

    display :badge do
        self.followed_by.count > 10 ? "Popular" : "--"
    end


    to_many :Tweet

    to_many   :User, to: :follows
    from_many :User, to: :follows, from: :followed_by


    action :follow, args: [{:user_id => :numeric}] do |context, user_id|
		self.follow(user_id)
	end

	action :unfollow, args: [{:user_id => :numeric}] do |context, user_id|
		self.unfollow(user_id)
	end

    action :post_tweet, args: [{:t => :text}], 
        guard: :guard_post_tweet do |context, t|
		    self.post_tweet(t)
	end


    document :stats do |context|
      self.statistics_report()
    end


    route_traversal :similar_users, return_parts: :User do
        self.similar_users.uniq
    end

    route_traversal :should_follow, return_parts: :User do
        self.recommended_users_to_follow(10)
    end


	module Route

    	def similar_users()
    		self.follows.followed_by.except(self)
    	end

    	def recommended_users_to_follow(limit)
    		self.similar_users.follows.except(self).except(self.follows).most_frequent(0...limit)
    	end

	end


    module Vertex

        def statistics_report
            {'follows_count'     => self.follows.count, 
             'followed_by_count' => self.followed_by.count}
        end

        def guard_post_tweet(errors, context, t)
            if t.length > 140
                errors.add(:t, "Tweet is too long.")
            end
        end

    	def post_tweet(t)
    		tweet = self.app.create(M::Tweet, {text: t})
			tweet.user = self
			tweet
    	end

    	def follow(user_id)
    		self.add_follows(self.app.graph.vertex(user_id, M::User))
    	end

    	def unfollow(user_id)
    		self.remove_follows(self.app.graph.vertex(user_id, M::User))
    	end

    end

  end
end

The full source code in lib/my_app/parts/tweet.rb

module MyApp
  module Tweet
    xn_part

    property :text, type: :text

    from_one :User

  end
end

Patterns

Working With Parts & Models

The Model Is The Part

  • The simple pattern we've seen so far.
  • No real distinction between a part or a model.
  • Good for defining simple types.

The Aspect

  • The part describes one aspect of the model.
  • For example, a Computer part might
  • Have properties like processor, memory, etc.
  • Have various relations.
  • Be a part of models such as Server, Desktop, SmartWatch or SmartPhone.

The Quality

  • The part describes a quality/feature of the model.
  • For example, a LivingCreature part can belong to models such as Person, Dog and Cat.
  • Allows efficient search by part.
    For example, find all living creatures.
  • Describes an aspect of the model, not a specific entity.
  • We can always ask whether a person is alive.
  • Although some people might be dead.
  • Chairs, on the other hand, don't have the notion of being dead or alive. 
  • Therefore, a Chair model would not have a LivingCreature part.

The Tag

  • The part indicates some quality/category of the model.
  • Similar to the previous pattern (aspect), only without any properties or relations.
  • For example, a Perishable part that belongs to models like Apple, Orange or Milk.

The Relator

  • A part whose sole purpose is to enable some relations.
  • For example:
  • We may want to attach notes to entities of different, unrelated types.
    Types such as PhoneSong, Person or Article.
  • We can define a HasNotes part with a relation to many Note objects.
  • Every model, with a HasNotes part, will have a relation to many notes.
  • Marked as                               .
not_descriptive!

Summary

  • Basic building blocks:
    • Part - A module with functionality.
    • Model - A Type, defined as a list of part.
  • Parts 
    • Properties & relations.
    • Instance methods (for vertices and routes).
    • Expose method to the API as actions and traversals.
  • API gives us:
    • CRUD operations for entities.
    • Following relations.
    • Actions for modifying relations.
  • Traversals are composable. 
    Extend the API with our domain-specific language.

Summary

Resources

XN - Data Modeling

By Joey Freund

XN - Data Modeling

The heart of XN - Flexible data modeling and feature-rich API.

  • 838