• Stars
    star
    71
  • Rank 428,222 (Top 9 %)
  • Language
    Crystal
  • License
    MIT License
  • Created almost 8 years ago
  • Updated over 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Ohm for Crystal

Ohm ॐ

Object-hash mapping library for Redis.

CI

Description

Ohm is a library for storing objects in Redis, a persistent key-value database. It has very good performance.

Community

Meet us on IRC: #ohm on freenode.net.

Related projects

These are libraries in other languages that were inspired by Ohm.

  • Ohm for Ruby, created by soveran
  • JOhm for Java, created by xetorthio
  • Lohm for Lua, created by slact
  • ohm.lua for Lua, created by amakawa
  • Nohm for Node.js, created by maritz
  • Redisco for Python, created by iamteem
  • redis3m for C++, created by luca3m
  • Ohmoc for Objective-C, created by seppo0010
  • Sohm for Lua, compatible with Twemproxy

Articles and Presentations

Getting started

Install Redis. On most platforms it's as easy as grabbing the sources, running make and then putting the redis-server binary in the PATH.

Once you have it installed, you can execute redis-server and it will run on localhost:6379 by default. Check the redis.conf file that comes with the sources if you want to change some settings.

Add this to your application's shard.yml:

dependencies:
  ohm:
    github: soveran/ohm-crystal

Or you can grab the code from http://github.com/soveran/ohm-crystal.

Connecting to a Redis database

Ohm uses a lightweight Redis client called Resp. To connect to a Redis database, you will need to set an instance of Resp, with an URL of the form redis://:<passwd>@<host>:<port>/<db>, through the Ohm.redis= method, e.g.

require "ohm"

Ohm.redis = Resp.new("redis://127.0.0.1:6379")

Ohm.redis.call "SET", "Foo", "Bar"

Ohm.redis.call "GET", "Foo"
# => "Bar"

Ohm defaults to a Resp connection to "redis://127.0.0.1:6379". The example above could be rewritten as:

require "ohm"

Ohm.redis.call "SET", "Foo", "Bar"

Ohm.redis.call "GET", "Foo"
# => "Bar"

All Ohm models inherit the same connection settings from Ohm.redis.

Models

Ohm's purpose in life is to map objects to a key value datastore. It doesn't need migrations or external schema definitions. Take a look at the example below:

Example

class Party < Ohm::Model
  attribute :name
  reference :venue, Venue
  set :participants, Person
  counter :votes

  index :name
end

class Venue < Ohm::Model
  attribute :name
  collection :parties, Party, :venue_id
end

class Person < Ohm::Model
  attribute :name
end

All models have the id attribute built in, you don't need to declare it.

This is how you interact with IDs:

party = Party.create({"name" => "Ohm Worldwide Party 2031"})
party.id
# => "1"

# Find an party by id
party == Party[1]
# => true

# Update an party
party.update({"name" => "Ohm Worldwide Party 2032"})
party.name
# => "Ohm Worldwide Party 2032"

# Trying to find a non existent party
Party[2]
# => nil

# Finding all the parties
Party.all.to_a
# => [<Party::0x102736570 name='Ohm Worldwide Party 2032'>]

This example shows some basic features, like attribute declarations and querying. Keep reading to find out what you can do with models.

Attribute types

Ohm::Model provides 4 attribute types:

  • Ohm::Model.attribute,
  • Ohm::Model.set
  • Ohm::Model.list
  • Ohm::Model.counter

and 2 meta types:

  • Ohm::Model.reference
  • Ohm::Model.collection.

attribute

An attribute is just any value that can be stored as a string. In the example above, we used this field to store the party's name. If you want to store any other data type, you have to convert it to a string first. Be aware that Redis will return a string when you retrieve the value.

set

A set in Redis is an unordered list, with an external behavior similar to that of Ruby arrays, but optimized for faster membership lookups. It's used internally by Ohm to keep track of the instances of each model and for generating and maintaining indexes.

list

A list is like an array in Ruby. It's perfectly suited for queues and for keeping elements in order.

counter

A counter is like a regular attribute, but the direct manipulation of the value is not allowed. You can retrieve, increase or decrease the value, but you can not assign it. In the example above, we used a counter attribute for tracking votes. As the increment and decrement operations are atomic, you can rest assured a vote won't be counted twice.

reference

It's a special kind of attribute that references another model. Internally, Ohm will keep a pointer to the model (its ID), but you get accessors that give you real instances. You can think of it as the model containing the foreign key to another model.

collection

Provides an accessor to search for all models that reference the current model.

Tracked keys

Besides the provided attribute types, it is possible to instruct Ohm to track arbitrary keys and tie them to the object's lifecycle.

For example:

class Log < Ohm::Model
  track :text

  def append(msg)
    key["text"].call("APPEND", msg)
  end

  def tail(n = 100)
    key["text"].call("GETRANGE", -n.to_s, "-1")
  end
end

log = Log.create
log.append("hello\n")

assert_equal "hello\n", log.tail

log.append("world\n")

assert_equal "world\n", log.tail(6)

When the log object is deleted, the :text key will be deleted too. Note that the key is scoped to that particular instance of Log, so if log.id is 42 then the key will be Log:42:text.

Persistence strategy

The attributes declared with attribute are only persisted after calling save.

Operations on attributes of type list, set and counter are possible only after the object is created (when it has an assigned id). Any operation on these kinds of attributes is performed immediately. This design yields better performance than buffering the operations and waiting for a call to save.

For most use cases, this pattern doesn't represent a problem. If you are saving the object, this will suffice:

if party.save
  party.comments.add(Comment.create({"body" => "Wonderful party!"}))
end

Working with Sets

Given the following model declaration:

class Party < Ohm::Model
  attribute :name
  set :attendees, Person
end

You can add instances of Person to the set of attendees with the add method:

party.attendees.add(Person.create({"name" => "Albert"}))

# And now...
party.attendees.each do |person|
  # ...do what you want with this person.
end

Working with Lists

Given the following model declaration:

class Queue < Ohm::Model
  attribute :name
  list :people, Person
end

You can add instances of Person to the list of people with the push method:

queue.people.push(Person.create({"name" => "Albert"}))

# And now...
queue.people.each do |person|
  # ...do what you want with this person.
end

Working with Counters

Given the following model declaration:

class Site < Ohm::Model
  attribute :url
  counter :visits
end

You can increment or decrement the visits:

site.visits     #=> 0
site.visits(+1) #=> 1
site.visits(+1) #=> 2
site.visits(+5) #=> 7
site.visits(-4) #=> 3
site.visits     #=> 3

Associations

Ohm lets you declare references and collections to represent associations.

class Post < Ohm::Model
  attribute :title
  attribute :body
  collection :comments, Comment, :post_id
end

class Comment < Ohm::Model
  attribute :body
  reference :post, Post
end

After this, every time you refer to post.comments you will be talking about instances of the model Comment. If you want to get a list of IDs you can use post.comments.ids.

References explained

Doing a Ohm::Model.reference is actually just a shortcut for the following:

# Redefining our model above
class Comment < Ohm::Model
  attribute :body
  attribute :post_id
  index :post_id

  def post=(post)
    self.post_id = post.id
  end

  def post
    Post[post_id]
  end
end

The net effect here is we can conveniently set and retrieve Post objects, and also search comments using the post_id index.

Comment.find({"post_id" => "1"})

Collections explained

The reason a Ohm::Model.reference and a Ohm::Model.collection go hand in hand, is that a collection is just a macro that defines a finder for you, and we know that to find a model by a field requires an Ohm::Model.index to be defined for the field you want to search.

Here's again the collection macro in use:

collection :comments, Comment, :post_id

When it expands, what you get is this method definition:

def comments
  Comment.find({"post_id" => self.id })
end

Both examples are equivalent.

Indices

An Ohm::Model.index is a set that's handled automatically by Ohm. For any index declared, Ohm maintains different sets of objects IDs for quick lookups.

In the Party example, the index on the name attribute will allow for searches like Party.find({"name" => "some value"}).

Note that the methods Ohm::Model::Set#find and Ohm::Model::Set#except need a corresponding index in order to work.

Finding records

You can find a collection of records with the find method:

# This returns a collection of users with the username "Albert"
User.find({"username" => "Albert"})

Filtering results

# Find all users from Argentina
User.find({"country" => "Argentina"})

# Find all active users from Argentina
User.find({"country" => "Argentina", "status" => "active"})

# Find all active users from Argentina and Uruguay
User.find({"status" => "active"}).combine({"country" => ["Argentina", "Uruguay"] })

# Find all users from Argentina, except those with a suspended account.
User.find({"country" => "Argentina"}).except({"status" => "suspended"})

# Find all users both from Argentina and Uruguay
User.find({"country" => "Argentina"}).union({"country" => "Uruguay"})

Note that calling these methods results in new sets being created on the fly. This is important so that you can perform further operations before reading the items to the client.

For more information, see SINTERSTORE, SDIFFSTORE and SUNIONSTORE.

Uniques

Uniques are similar to indices except that there can only be one record per entry. The canonical example of course would be the email of your user, e.g.

class User < Ohm::Model
  attribute :email
  unique :email
end

u = User.create({"email" => "[email protected]"})
u == User.with("email", "[email protected]")
# => true

User.create({"email" => "[email protected]"})
# => raises Ohm::UniqueIndexViolation

More Repositories

1

cuba

Rum based microframework for web development.
Ruby
1,429
star
2

ohm

Object-Hash Mapping for Redis
Ruby
1,390
star
3

micromachine

Minimal Finite State Machine
Ruby
522
star
4

clac

Command-line, stack-based calculator with postfix notation
C
350
star
5

map

Map lines from stdin to commands
C
221
star
6

mote

Minimum Operational Template
Ruby
216
star
7

nest

Generate nested namespaced keys for key-value databases.
Ruby
185
star
8

ost

Redis based queues and workers.
Ruby
166
star
9

toro

Tree oriented routing
Crystal
144
star
10

syro

Simple router for web applications
Ruby
135
star
11

cargo

Require libraries without cluttering your namespace.
Ruby
127
star
12

scrivener

Validation frontend for models.
Ruby
124
star
13

clap

Command line argument parsing
Ruby
90
star
14

disque-rb

Disque client for Ruby
Ruby
68
star
15

gs

Gemset management
Ruby
67
star
16

totp

Time-based One-Time Passwords
Ruby
44
star
17

mailcat

Fake SMTP server that prints emails to stdout
C
38
star
18

chen

Change directory entries with your text editor
C
37
star
19

finist

Redis based Finite State Machine
Ruby
36
star
20

spawn

A ridiculously simple fixtures replacement for your web framework of choice.
Ruby
36
star
21

redisurl

Connect to Redis using a REDIS_URL and the redigo client.
Go
33
star
22

hart

Hash router
Ruby
24
star
23

redisent

Sentinels aware Redis client.
Ruby
22
star
24

stal-ruby

Set algebra solver for Redis
Ruby
22
star
25

relay

Relay commands over SSH
Ruby
21
star
26

resp

Lightweight RESP client for Lua
Lua
21
star
27

lomo

Apply a lomo filter to your pictures from the command line using ImageMagick.
Ruby
19
star
28

override

The as-simple-as-possible-but-not-simpler stubbing library.
Ruby
16
star
29

ox

Skeleton for a Cuba-based JSON API.
Ruby
16
star
30

terco

Obstinate DNS
Ruby
16
star
31

nido

Structured keys helper
Ruby
14
star
32

syro-demo

Demo application with Syro
Ruby
14
star
33

resp-crystal

Lightweight RESP client
Crystal
14
star
34

mt

Mail tester daemon.
Ruby
13
star
35

stringent

Generate a string with a target entropy
Ruby
12
star
36

rediscan

Scanner for Redis keyspace
Ruby
12
star
37

hypertext

Hypertext authoring with Ruby
Ruby
11
star
38

cuba-book

Cuba Book
11
star
39

sc

List of HTTP status codes.
Ruby
11
star
40

filmo

A single page presentation tool.
11
star
41

drawer

Ultra slim file-based cache
Ruby
10
star
42

finist.lua

Redis based Finite State Machine
Lua
10
star
43

nest-crystal

Object Oriented Keys for Redis
Crystal
9
star
44

basica

Basic authentication library.
Ruby
8
star
45

rino

Remove a file by its inode number
C
7
star
46

seg.rb

Segment matcher for paths
Ruby
7
star
47

trim

Read from stdin and remove a prefix from each line
C
7
star
48

seg

Segment matcher for paths
Crystal
7
star
49

tas

Trees as strings
Ruby
6
star
50

stal

Set algebra solver for Redis
Lua
6
star
51

pac

Package management for Lua libraries.
Shell
6
star
52

prep

Read from stdin and prepend a string to each line while preserving identation.
C
6
star
53

stal-crystal

Set algebra solver for Redis
Crystal
5
star
54

m4s2

Static Site Generator
HTML
5
star
55

contract

Contract helper
Ruby
5
star
56

rel

Ruby client for the Bandicoot database.
Ruby
4
star
57

ook

Object Oriented Keys for Redis
Ruby
4
star
58

loco

Lines of code counter
C
4
star
59

homebrew-tools

Formulae for Homebrew
Ruby
4
star
60

walk

Walk a directory tree and print the name of every regular file
C
4
star
61

textorama

Text slides for the terminal
Shell
4
star
62

rediscan.lua

Scanner for Redis keyspace in Lua
Lua
3
star
63

seg.go

Segment matcher for paths
Go
3
star
64

look

Add a vendor directory to your load path.
Ruby
3
star
65

app

Cuba application template for gn, the file generator.
Ruby
2
star
66

packer

Require different versions of the same Cargo-compatible gem
Ruby
2
star
67

tele.sh

2
star
68

ers

AWK script easy replacements in templates
Awk
2
star
69

soveran.github.io

2
star
70

filter

Workflow template for gn.
Ruby
1
star
71

remoto

Ruby
1
star
72

ohm-scripts

Lua scripts for Ohm compatible libraries
Lua
1
star
73

gem

Gem template for gn, the file generator.
Ruby
1
star
74

ostgo

Ost client.
Go
1
star
75

miga

Breadcrumb name of the working directory
C
1
star