Skip to content
This repository has been archived by the owner on Feb 5, 2019. It is now read-only.

Tutorial: Storing persistent data

gitbase edited this page Jun 1, 2011 · 11 revisions

This document is a tutorial on how to use Autumn’s DataMapper integration to add persistent storage to your leaf. If you’d like an overview of DataMapper, please visit its website. If you’d like an overview of DataMapper integration in Autumn, consult the README.

To use DataMapper you may need to edit the Gemfile and change the DataMapper adapter to one suited for your database (by default it’s dm-sqlite-adapter).

Autumn already includes an example leaf, Scorekeeper, complete with a full data model. However, I realize it can be a bit daunting to jump right into the middle of that, especially since Scorekeeper is not a simple leaf… So let’s start from the beginning.

In this tutorial, I’ll show you how to build a very simple bot that remembers the last time each channel member said something. People will be able to query it to figure out if their friends were recently talking in IRC.

Step 1: Configuration

Let’s generate our leaf first by typing script/generate leaf last_online. A new leaf directory, last_online, will be created in the leaves directory. We need to add this leaf to our stem, so open the config/seasons/[season]/leaves.yml file (if you are using a leaves.yml file) and add an entry for your leaf:


LastOnline:
  class: LastOnline

We’ve named the leaf after its class, to keep things simple. Now we need to configure the database connection. In the database.yml file, add an entry for our new leaf “LastOnline”. In this example I will use SQLite 3, but you can set up the database for MySQL or PostgreSQL if you prefer.


LastOnline: sqlite:leaves/last_online/data/development.db

I’d then create the database with sqlite3 leaves/last_online/data/development.db.

I’ve named my database connection after my leaf so they’ll be associated automatically. See the README for a more in-depth discussion on naming database connections.

Step 2: Defining Our Models

To keep things extra-simple, we’re going to have only one table and therefore only one model object. This is therefore not a spectacular way to learn about DataMapper’s many features, like associations and lazy loading and what-have-you. Again, visit the DataMapper website to learn about DataMapper’s features; this tutorial just shows you how to use DataMapper in Autumn.

Anyway, we need three pieces of information to form a complete picture on when someone last talked in IRC: The time they talked, their nick, and the channel they were in. That’s the minimum, but for good fun, we’ll throw in a fourth column: what they said. (Maybe it’s something helpful like, “Going to the gym, be back at 7.”)

DataMapper is an active-record ORM (like Ruby on Rails’s ActiveRecord class), so in general, one model object corresponds to one table and one attribute of that object corresponds to one column in the table.

We’re going to define the table by creating a new mode object called Event. This will automatically correspond to a table called events. So, first off, create a file at leaves/last_online/models/event.rb. In it, create your class, and add the four properties specified above:


class Event
  include DataMapper::Resource

  property :nick, String, :nullable => false, :limit => 16
  property :channel, String, :nullable => false, :limit => 16
  property :message, String, :nullable => false
  property :updated_at, DateTime, :default => 'NOW()'
end

(Different IRC servers have different length limits on nick and channel names; make sure 16 characters is enough for your server.)

Unlike with ActiveRecord, the updated_at column will not be set automatically; we will have to do it manually. (Alternatively you could use the dm-timestamps gem to get it automagically.)

With our leaf’s models defined we can now populate the database. Type DB=LastOnline rake db:migrate to turn the model objects into database tables. Verify that the table has been added to your database and its columns are correct.

Step 3. Writing the Leaf

Our leaf will respond to the “!seen” command. The command will be followed with a nickname. The leaf will then return the last thing that person said in the sender’s channel, along with a timestamp. Here’s an example:

(in channel #flowers)

<Someone> !seen Sancho
<LastOnline> [10/19/07 10:15 PM] <Sancho> have fun!

(in channel #honeybees)

<Someone> !seen Sancho
<LastOnline> Sancho has not been seen in this channel.

So, let’s write this “!seen” command:


def seen_command(stem, sender, reply_to, msg)
  unless stem.nick? msg
    render :invalid_nick
    return
  end
  event = Event.first(:channel.eql => reply_to, :nick.eql => msg)
  var :name => msg if event.nil?
  var :event => event
end

This method finds the first row in the events table that has a matching channel and nick combination, then passes it to the format method, which we will write next. It only accepts valid nicks. Now let’s write a view in the views/seen.txt.erb file:


<% if var(:event) %>
[<%= var(:event).updated_at.strftime '%m/%d/%y %I:%M %p' %>] <<%= var(:event).nick %>> <%= var(:event).message %>
<% else %>
<%= var :name %> has not been seen in this channel.
<% end %>

And we’ll need to add the invalid_nick view as well, in views/invalid_nick.txt.erb:


I need a nick to look up.

Now that we’ve handled reading these events out from the database, we of course need to deal with writing them into the database. To do this, we will override the did_receive_channel_message method to write into the database each time a new message is received.


def did_receive_channel_message(stem, sender, channel, msg)
  event = Event.find_or_create :channel => channel, :nick => sender[:nick]
  event.message = msg
  event.updated_at = Time.now
  event.save
end

I realize the database-savvy among you are cringing at the thought of both reading from and writing to the database (up to twice) each and every time a message is received, but this is a K.I.S.S. example. The creation of an in-memory cache that periodically flushes to DB is left as as exercise for the reader.

Anyway, that Event#find_or_create call either finds us or makes for us a row containing the channel and nick of the sender. That’s all we need to uniquely identify a row in our table schema. Given that row, we set the other fields, and save the record.

And that’s it! Our leaf is complete!

Awesome! Now What?

Although for the sake of example, this leaf is finished, it actually has a long way to go.

First off, it’s not very smart about nicks. Sancho could change his nick to “Sancho|eating”, and the bot would think it’s a completely different person. I’d recommend checking the hostmask or stripping the “|eating” part out.

Secondly, while the database schema is (mostly) solid, our usage of the database is pretty bad. As I mentioned with regards to the did_receive_channel_message method, it would probably be a good idea to store these new records in memory, only writing them to database periodically (say, every 2 minutes).

Even without doing that, the did_receive_channel_message method can be optimized. The find_or_create call is one DB lookup and possibly one insert; the save call is yet another insert. That means we are doing three DB operations when we really should be doing one or two.

Finally, we should probably spawn off a separate thread to purge old entries from the database. If someone logs into the IRC server once, and never again; or if someone changes their nick to something like “sdgjsfdhie” to say one thing, they will get an entry in the database. That entry will sit there for all eternity. It’s easy to see how your DB could grow very large over time. A reaper thread would delete rows with updated_at columns earlier than, say, a couple weeks. After all, if you want to know when your friend was last online, beyond a couple of weeks, what difference does it make? He was online “a long time ago,” for all intents and purposes.

These are of course the standard concerns you must address when you bring your leaf wide-eyed into the world of relational databases. DataMapper will help you get your persistent storage off the ground quickly and painlessly.