Using the Database

  • [code]
  • [database]

Most commands will need to read data from or save data to the database. Ares uses an off-the-shelf database tool called Redis. The Ohm database library lets you interact with the database directly from Ruby code.

Models

Ohm lets you define ruby classes that interact with the database. These are called Models, and you can identify them by the fact that they inherit from Ohm::Model. Model classes define attributes that correspond to fields in the database. For example, the Character class defines a name and alias:

class Character < Ohm::Model
  attribute :name
  attribute :alias
  ...
end

You’ve probably seen database fields used throughout the code, in examples like client.emit "Hello #{enactor.name}!" In that case, enactor is a character model, and name is the database field.

You’ll need to look in the code to find out what database fields are available.

Custom Models

To create a custom database model, you just need to define a class that inherits from Ohm::Model.

CORRECT

module AresMUSH
  class MyThing < Ohm::Model
  end
end

INCORRECT

module AresMUSH
  module SomePlugin
    class MyThing < Ohm::Model
    end
  end
end

Once you have your model class, you can set up your own attributes. For example:

module AresMUSH
  class MyThing < Ohm::Model
    attribute :name
    attribute :some_field
  end
end

Game Model

The Game model is special because there’s only one game. You don’t really need to search for it. You can access the one and only game model using:

Game.master

Instances

The model class defines the data for all objects of that type. For example, the Character model class defines the data for all characters and MailMessage defines the data for all mail messages.

An instance is a specific occurrence of that model–a specific character, or a specific mail message.

You can create an instance of a model using ClassName.create, and optionally pass attribute/field data. For example:

char = Character.create(name: "Jack")

The variable char now refers to your new database object instance, so you can use that instance to read and update fields, as described in the next section.

Deleting an object is as simple as:

char.delete

Read on for more information about finding and updating objects.

Instance IDs

All Ohm models have an implicit id field that identifies a specific instance. This is just an auto-incrementing integer.

char = Character.find_one_by_name("Bob")
client.emit "Their ID is #{char.id}!"

Unlike in older MUSH codebases, IDs are per model type. Character 9 is a completely different object than Room 9.

Fields

Once you have a database object instance, you can use its properties (aka fields or attributes) as simple methods.

Fields are defined using the attribute keyword on the model class. For example, the Character class has a name field:

class Character < Ohm::Model
  attribute :name
  ...
end

Getting Field Data

You can read properties just by using the attribute name like a method. For example:

char = Character.find_one_by_name("Bob")
client.emit "Hello #{char.name}!"

Updating Fields

You can update the properties on a database object instance using the update method. For example:

char = Character.find_one_by_name("Bob")
char.update(name: "Harry")

Many database properties have specialized update methods because their data storage is not so straightforward. Look around for helper methods in the plugin module:

char = Character.find_one_by_name("Bob")
Demographics.set_group(char, "Faction", "Navy")

Field Types

Under the hood, all redis fields are stored as strings. The Ohm database library provides some utilities to let you tell the Ruby code to interpret certain fields as different data types, as shown below:

attribute :team, :type => DataType::Integer
attribute :freshly_damaged, :type => DataType::Boolean
attribute :armor_specials, :type => DataType::Array, :default => []
attribute :prior_ammo, :type => DataType::Hash, :default => {}
attribute :starts, :type => DataType::Time
attribute :birthdate, :type => DataType::Date    

Queries

The Ohm database library provides a variety of ways to query for database models, but here are a few of the easiest:

  • Character[id] - Finds an object directly by ID.
  • Character.find_one_by_name(name) - Finds the first character with that name/alias.
  • Character.find_any_by_name(name) - Finds all characters with that name/alias.
  • Character.all - Finds all characters.

If your database isn’t gigantic, you can also use the Ruby select statement to filter objects based on any set of criteria. If your database is huge, you’re better off using Ohm’s queries. They’re a little more complex, but faster. Here’s an example that will find all characters with the ‘admin’ role:

def handle
  chars = Character.all.select { |c| c.has_any_role?("admin") }
  names = chars.map { |c| c.name }
  client.emit "You found #{names.join(", ")}"
end

Finder Helpers

There are a few convenient utilities to help with common searches. All of these will search by name or (for players) by alias, and include the me or here keywords, so they require you to pass in the character doing the search.

  • ClassTargetFinder - Searches for a single object of the specified type.
  • AnyTargetFinder - Searches for a single object among rooms, characters or exits. Helpful for commands like describe/destroy that can work on multiple object types.
  • OnlineCharFinder - Searches for a single matching online character.
  • VisibleTargetFinder - Searches for a matching object in the same room - either a character, room or exit.

All of the finder helpers do the error handling necessary to translate multiple matches into a message like “I don’t know which one you mean” and no matches into “I don’t see that here”. They return a FindResult object containing a ‘found’ indication and either the target object or an error message.

result = ClassTargetFinder.find(name, Character, searcher)
if (result.found?)
    # Object was found - do something with result.target
else
   # Object was not found - do something with result.error
end

Target Finder Block helpers

Several of the more commonly-used target finders have another level of helper utility you can use. Here’s how it works:

ClassTargetFinder.with_a_character(name, client, enactor) do |model|
    # Do something with model if it's found
end

with_a_character will do the stuff in-between the do and the end if the character was found, using the variable model for the character it found. If the character was not found, it will emit the appropriate error message to the client.

There’s a similar helpers for with_something_visible and with_online_chars.

Callback Methods

Some database models use Callbacks - methods that are triggered in response to certain database events. There are two possible callbacks in Ares, called before an object is deleted or before it’s saved:

  • before_delete - If an object has any references, you may want to delete them before the object itself is deleted. For example, a room might delete all of its exits when it’s getting deleted.
  • before_save - If an object has special fields, you may set them before the object is saved. For example, if you store the uppercase version of the object name for fast lookups, you want to update that to match the object name every time the object is saved.

Relationships - References, Sets and Collections

Although Redis is not a relational database like SQL, we still need to define relationships between our objects. A mail message has an author and recipients (all characters), a scene is attached to a room, etc. Ohm provides some built in concepts that let us manage these relationships. See Database Relationships.