Semi-Relational Data Modeling with Redis and Ohm

Share this article

Semi-Relational Data Modeling with Redis and Ohm
ohm

You probably know about Redis. If you’ve never used it, chances are you think of it as another in-memory data-store. But if you think of it like that, you’ll be doing it an injustice: Redis is much more than just a key-value store. It’s a data-structure server including an in-memory dataset. Redis goes beyond the capabilities of a mere key-value store by supporting:

  • Data structures such as Lists, Sets, and Hashes
  • Persistence, in both snapshot and log-based modes
  • Pub/Sub mechanism and -by extension- message queue implementations
  • Clustering, partitioning, and high availability (via [Sentinel] (http://redis.io/topics/sentinel))

In this tutorial, we’ll create an Object Model that wraps itself around Redis’s data structures, providing us with a way to seamlessly interact with Redis from within our Ruby code. This is provided by the excellent Ohm gem.

Furthermore, we’ll see how we can use this Object Model to emulate some ActiveRecord-like functionality. Before we begin though, let’s get one thing straight: Redis isn’t a relational database and Ohm isn’t an ORM. If you’re expecting to use Redis/Ohm for things like transactions and cascading referential integrity you’ll be bitterly disappointed.

If, however, you want to store and retrieve loosely related entities (think aggregates, not composites) in a blindingly fast manner with the added bonus of data partitioning, high availability, and pub/sub capabilities, then Redis and Ohm were made for you.

Ground Rules

This tutorial does not intend to cover the full range of Ohm or Redis features. It’ll cover a small subset needed to understand and implement the most common data modeling use cases. Ohm works on two facets:

  • Managing objects in the Ruby memory space
  • Managing Redis hashes, indices, and other data structures that correspond to those Ruby objects

The synchronization between the two isn’t always obvious, but Ohm offers a variety of methods to manage the synchronicity either implicitly or explicitly. In this tutorial, we’ll prefer the implicit methods, giving us the option of assuming that any operation on our Ohm objects will have an equivalent impact on the corresponding Redis data structures (unless otherwise stated). When, for instance, we update an object’s attribute, we expect the equivalent Redis entries to have been updated accordingly.

Setting Up

We obviously need to install Redis and ensure it’s up and running. Follow the instructions here and get Redis working on your machine.

Also, as mentioned, we’ll be taking advantage of the Ohm gem, so make sure it’s installed:

$ gem install ohm

Defining Classes

For the purpose of this tutorial, we’ll model an authoring system. Define two classes: an Author and a Book class, both derived from Ohm::Model. An Author may produce 0-to-many books. Assume that a Book can only belong to one Author.

A very basic implementation of the system looks as follows:

require 'ohm'

class Author < Ohm::Model
  attribute :f_name
  attribute :l_name
  attribute :email
  attribute :age
end

class Book < Ohm::Model
  attribute :title
end

Attributes

We defined our core class attributes using the attribute class macro. Attribute values are defined as strings, but we can also pass a lambda as a second argument to the macro which can serve as a way of simple typecasting:

attribute :age, lambda { |x| x.to_i }

The above ensures that the age attribute is always returned as an Integer, even if we set it as a String.

To set the value of an attribute, use the update instance method:

irb> author.update(email: 'fred@gmail.com')
#=> <Author>

To get the value of an attribute, use dot notation, as usual:

irb> author.email
#=> fred@gmail.com

The dot notation will get the attribute value from our locally cached Author object. To get the value directly from Redis, use the get instance method:

irb> author.get(:email)
#=> fred@gmail.com

Indexing

We can index attributes with the index class macro. This makes attributes searchable by using the find class method. Let’s make sure we can search for authors by their last_name:

class Author < Ohm::Model
  index :l_name
end

This is now possible:

irb> author = Author.create(f_name: "Joe", l_name: "Bloggs", age: 34, email: "jbloggs@gmail.com")
irb> Author.find(l_name: "Bloggs").size
#=> 1

The find method returns an Ohm::Set object. An Ohm::Set is an unordered list with external behavior similar to that of Ruby arrays. Just like arrays, information can be extracted using familiar methods like first:

irb> Author.find(l_name: "Bloggs").first.email
#=> "jbloggs@gmail.com"

Uniques

If we want an attribute to have a unique value, set it as unique. In our Author example, the email attribute is probably a good candidate to be marked as unique:

class Author < Ohm::Model
  unique :email
end

Now, creating an Author with the same email value as an existing Author will result in a Ohm::UniqueIndexViolation error.

Model Creation

Just like in ActiveRecord, we use the create class method to create a new model object:

irb> author = Author.create(f_name: "John", l_name: "Smith", age: 34, email: "jsmith@gmail.com")

which returns:

#=> #<Author:0x000000023c8118 @attributes={:f_name=>"John", :l_name=>"Smith", :age=>34, :email=>"jsmith@gmail.com"}, @_memo={}, @id="1">

One thing that’s immediately obvious is that Ohm added an id attribute to our new object. This is equivalent to a relational database’s primary key: a unique identifier for that type of object within the current Redis database. We can now use the id attribute value with the [] class method to create an instance with the same attributes whenever we need it:

irb> a = Author[1]
#=> #<Author:0x000000021db990 @attributes={:f_name=>"John", :l_name=>"Smith", :age=>34, :email=>"jsmith@gmail.com"}, @_memo={}, @id="1">

Note that each time we retrieve an Author object based on its id, a new Author object is being created with the same attributes as our targeted object. As such, we can have many Ruby Ohm::Model objects all referring to the same Redis-stored entity.

Deleting and Expiring Keys

Delete an object by calling its delete method, i.e. object.delete. Bear in mind two things when using #delete:

  1. It will delete the object keys from Redis, including uniques, indices, and any sets the object may contain. It will not, however, delete the objects referenced by the object’s sets. These will continue to live happily in Redis’s memory until they either expire, are forced out by Redis’s eviction policy, or are deleted by having their #delete method called.
  2. It will delete the object keys from Redis, but it will not delete the Ruby object from our application’s memory space. Furthermore, the remaining Ruby object can be used to “reanimate” the deleted Redis object keys, producing the following phenomenon:

    irb> author = Author.create(f_name: "John", l_name: "Smith", age: 34, email: "abc@gmail.com")
    => #<Author:0x00000001e48e68 @attributes={:f_name=>"John", :l_name=>"Smith", :age=>34, :email=>"abc@gmail.com"}, @_memo={}, @id="13">
    
    
    irb> Author[13] #check that new author is stored in Redis
    => #<Author:0x00000001e2f238 @attributes={:f_name=>"John", :l_name=>"Smith", :email=>"abc@gmail.com", :age=>"34"}, @_memo={}, @id=13>
    irb> author.delete #delete author from Redis
    => #<Author:0x00000001e48e68 @attributes={:f_name=>"John", :l_name=>"Smith", :age=>34, :email=>"abc@gmail.com"}, @_memo={}, @id="13">
    irb> Author[13] #author not present in Redis anymore
    => nil
    irb> author.update(f_name: 'Jack') #we use the author object to update an attribute
    => #<Author:0x00000001e48e68 @attributes={:f_name=>"Jack", :l_name=>"Smith", :age=>34, :email=>"abc@gmail.com"}, @_memo={}, @id="13">
    irb> Author[13] #surprise! the author has been re-created in Redis
    => #<Author:0x00000001cffe58 @attributes={:f_name=>"Jack", :l_name=>"Smith", :email=>"abc@gmail.com", :age=>"34"}, @_memo={}, @id=13>

As long as you’re aware of this behavior, you can use delete as needed. However, it’s safe practice to have a good eviction policy setting in the redis.conf file. A good bet would be:

maxmemory 512mb
maxmemory-policy volatile-lru

This causes Redis to start deleting keys as soon as the 512 (choose your own limit here) megabyte memory limit is reached. Keys will be deleted on a least-recently-used basis only if they have an expire set, which is useful if your Redis database holds mixed data (i.e. not only Ohm data but also some sort of caching data from other applications). If the Redis database’s only purpose is to keep relational data and the timing of the expires is critical, then the noeviction policy setting is recommended.

Associations

Ohm::Model allows us to to define relationships between objects using the #reference and #collection macros. The collection macro is equivalent to ActiveRecord’s has_many method, while reference roughly mimics ActiveRecord’s has_a or belongs_to methods. Add these macros to the existing classes:

require 'ohm'

class Author < Ohm::Model
  attribute :f_name
  attribute :l_name
  attribute :email
  attribute :age, lambda { |x| x.to_i }
  index :l_name
  unique :email

  collection :books, :Book
end

class Book < Ohm::Model
  attribute :title
  reference :author, :Author
end

Create an author and a couple of books:

irb> author = Author.create(f_name: "John", l_name: "Smith", age: 34, email: "jsmith@gmail.com")
=> #<Author:0x0000000279a480 @attributes={:f_name=>"John", :l_name=>"Smith", :age=>34, :email=>"jsmith@gmail.com"}, @_memo={}, @id="1">

irb> book1 = Book.create(title: 'Moby Dick')
=> #<Book:0x00000002776fa8 @attributes={:title=>"Moby Dick"}, @_memo={}, @id="1">

irb> book2 = Book.create(title: 'The Hobbit')
=> #<Book:0x000000026348c0 @attributes={:title=>"The Hobbit"}, @_memo={}, @id="2">

The reference macro has given our book objects an author attribute, which we can use to set the author of our books:

irb> book1.update(author: author)
=> #<Book:0x00000002776fa8 @attributes={:title=>"Moby Dick", :author_id=>"1"}, @_memo={}, @id="1">
irb> book2.update(author: author)
=> #<Book:0x000000026348c0 @attributes={:title=>"The Hobbit", :author_id=>"1"}, @_memo={}, @id="2">

Ohm has added a new author_id attribute to the book objects. This is a foreign key attribute referencing the author associated with this book. The collection macro used in the Author class ensures that the author can track all their books:

irb> author.books.each {|b| puts b.title}
=> Moby Dick
=> The Hobbit

In addition, we can use some of Ohm::Set‘s sorting and finding methods, like #include?, #find, #sort, and #sort_by to filter and sort the book collection.

Talking Directly to Redis

If you ever need to run a Redis command that isn’t abstracted by an Ohm method, use the redis.call method:

irb> Ohm.redis.call "FLUSHDB"
=> "OK"

Here, we’re telling Redis to clear the current database. We can run any of the Redis-cli commands in this manner from our Ruby code.

Summary

The Ohm – Redis combination offers great potential, of which only a small amount is described here. With very little effort we can leverage loads of powerful NoSQL features with a touch of relational goodness, a perfect combination for many modern applications.

Frequently Asked Questions on Semi-Relational Data Modeling with Redis and OHM

What is semi-relational data modeling in Redis?

Semi-relational data modeling in Redis is a unique approach to data organization that combines the principles of relational and non-relational databases. It leverages the flexibility of Redis, a NoSQL database, while incorporating some aspects of relational databases. This model allows for efficient data retrieval and manipulation, especially in applications that require real-time data processing. It uses Redis’ data structures like strings, hashes, lists, sets, and sorted sets, and also allows for relationships between different data items, similar to a relational database.

How does OHM assist in semi-relational data modeling?

Object Hash Mapping (OHM) is a library for Redis that provides an interface for managing data. It allows developers to create, retrieve, update, and delete data in Redis in an object-oriented manner. OHM plays a crucial role in semi-relational data modeling by providing a layer of abstraction that simplifies the process of managing data in Redis. It allows for the creation of models that represent data objects, and these models can have attributes and relationships, similar to tables in a relational database.

How does semi-relational data modeling compare to the relational model in DBMS?

While both semi-relational data modeling and the relational model in DBMS are used for organizing data, they have some key differences. The relational model in DBMS is based on tables, rows, and columns, and it uses SQL for data manipulation. On the other hand, semi-relational data modeling in Redis uses data structures like hashes and lists, and it uses commands specific to Redis for data manipulation. However, with the help of OHM, Redis can also support relationships between data items, making it semi-relational.

What are the benefits of using semi-relational data modeling in Redis?

Semi-relational data modeling in Redis offers several benefits. It provides the flexibility of a NoSQL database, allowing for efficient handling of large volumes of data. It also supports relationships between data items, which can be beneficial in applications that require complex data manipulation. Additionally, with the help of OHM, it provides an object-oriented interface for managing data, making it easier for developers to work with.

How can I implement semi-relational data modeling in Redis?

To implement semi-relational data modeling in Redis, you need to use the OHM library. First, you need to define models that represent your data objects. These models can have attributes and relationships. Then, you can use OHM’s methods to create, retrieve, update, and delete data in Redis. You can also use Redis’ commands to manipulate data directly.

Can I use semi-relational data modeling in Redis for building a Twitter clone?

Yes, you can use semi-relational data modeling in Redis for building a Twitter clone. Redis’ data structures like lists and sets can be used to store tweets and user information, and its support for relationships can be used to implement features like following and followers. With the help of OHM, you can manage this data in an object-oriented manner.

What are the limitations of semi-relational data modeling in Redis?

While semi-relational data modeling in Redis offers many benefits, it also has some limitations. It does not support all the features of a fully relational database, such as complex joins and transactions. Also, while OHM simplifies data management, it also adds a layer of abstraction that can potentially impact performance.

How does semi-relational data modeling in Redis compare to other NoSQL data modeling techniques?

Semi-relational data modeling in Redis provides a unique combination of flexibility and structure. Unlike other NoSQL data modeling techniques that are purely non-relational, it supports relationships between data items. This can be beneficial in applications that require complex data manipulation. However, it still maintains the performance and scalability benefits of a NoSQL database.

Can I use SQL with semi-relational data modeling in Redis?

No, you cannot use SQL with semi-relational data modeling in Redis. Redis uses its own set of commands for data manipulation. However, with the help of OHM, you can manage data in an object-oriented manner, which can be similar to working with a relational database.

Is semi-relational data modeling in Redis suitable for all types of applications?

Semi-relational data modeling in Redis is particularly suitable for applications that require real-time data processing and can benefit from the flexibility of a NoSQL database. However, for applications that require complex joins and transactions, a fully relational database might be more suitable. As with any technology choice, it depends on the specific requirements of your application.

Fred HeathFred Heath
View Author

Fred is a software jack of all trades, having worked at every stage of the software development life-cycle. He loves: solving tricky problems, Ruby, Agile methods, meta-programming, Behaviour-Driven Development, the semantic web. Fred works as a freelance developer, and consultant, speaks at conferences and blogs.

GlennG
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week