
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
- Tweet
- text
- User
- 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
- Defines the name of a to-relation.
- 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
- Example:
- Inconvenient to implement graph queries.
- Hard to debug.
- Potentially limiting.
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:
- Defining the logic.
- 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
- Use the similar_users method and follows relation.
- Exclude myself, and users I am already following, from the result.
- 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 Phone, Song, 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