Lafcadio


Table of Contents

1. Introduction
2. Installation
Installation overview
Install MySQL/Ruby (or Ruby/MySQL)
Install Ruby-DBI
Install Lafcadio
... with RubyGems
... without RubyGems
Test Lafcadio (optional)
3. Tutorial
Create a configuration file
Create a table, and corresponding domain class
Create, update, and delete Users
Create a new user and save it to the database
Retrieve a user and change some values
Delete the user
Add methods to the User class
Record an association between Users and Messages
Query Users
4. Matching database tables to domain classes
Defining domain class fields
binary
boolean
date
date_time
domain_object
email
enum
float
integer
month
state
string
text_list
Naming assumptions, and how to override them
Domain class inheritance
5. Using domain classes
Inserting, updating, and deleting
Dynamic associations
Triggers
6. Querying
Building and accessing queries
Query inference operators
Numerical comparisons: lt, lte, gte, gt
Equality: equals
Inclusion: in and include?
Text comparison: like
Compound conditions: & and |
Negation: not
Hands-on query construction
Query caching via subset matching
Eager loading
7. Testing
Setting up testing in a Lafcadio-based program
Domain objects in test cases
Quicker ways to setup mock objects
8. Advanced ObjectStore tricks
Transactions
The ObjectStore cache
9. Credits

Chapter 1. Introduction

Lafcadio is an object-relational mapping library for Ruby. The point of an ORM is to allow you to treat database rows like first-class objects, minimizing the amount of time you have to spend thinking about SQL vagaries so you can spend more time thinking about your program's logic. It currently supports MySQL and PostgreSQL.

Features include:

  • Strong support for testing with a full-featured mock database and dead-simple dependency injection (via ContextualService).
  • A rich query inference language that takes full advantage of Ruby's reflective nature. You can use this to create database queries that run against both the real database and the mock database. You can also use this to build complex queries without the annoying and error-prone process of concatenating SQL strings.
  • Heavy support for legacy databases. Lafcadio's attitude towards schemas is fairly pragmatic: Although it's nice to be able to create a schema from scratch, there are a lot of legacy schemas out there that still need support. Lafcadio goes quite far in supporting those older schemas. If you've got a boolean field whose values are "y" and "n" instead of 0 and 1, Lafcadio can handle that. If you've got a field that stores an array as a comma-delimited list, Lafcadio can handle that too.

In development since 2002, Lafcadio is still in active development, but is mature enough to be used in production systems. Most notably, it's used at Rhizome.org, an online arts website that gets more than a million pageviews a month.

Chapter 2. Installation

Installation overview

Because Lafcadio depends on Ruby-DBI, and Ruby-DBI depends on MySQL/Ruby, an installation of Lafcadio requires at least three separate steps: Installing MySQL/Ruby, installing Ruby-DBI, and installing Lafcadio. You may already have installed the first two parts installed, but in case you haven't I've included install instructions here.

Install MySQL/Ruby (or Ruby/MySQL)

First install MySQL/Ruby. The following will work for most setups:

% curl http://tmtm.org/downloads/mysql/ruby/mysql-ruby-2.7.tar.gz -o mysql-ruby-2.7.tar.gz
% tar zxvf mysql-ruby-2.7.tar.gz
% cd mysql-ruby-2.7
% ruby extconf.rb --with-mysql-config
% make
% ruby test.rb [hostname] [username] [dbpassword]
% make install     # as superuser

If your configuration is slightly different, check the MySQL/Ruby page for more detailed setup info.

Note

Please note that MySQL/Ruby has some known issues on certain architectures. If you have strange segfaults while running MySQL/Ruby tests, we recommend that you install Ruby/MySQL instead. Make sure that MySQL/Ruby is uninstalled, so that Ruby-DBI will unambiguously know which library to load.

In case you're wondering: MySQL/Ruby and Ruby/MySQL, both written by Tomita Masahiro, are two implementations of the same API in C and pure Ruby, respectively. Ruby/MySQL is mildly slower, but both look the same from the outside, and to Ruby-DBI.

Install Ruby-DBI

Ruby-DBI offers a generic DB connection interface. To install, try the following:

% curl http://rubyforge.org/frs/download.php/655/ruby-dbi-all-0.0.23.tar.gz -o ruby-dbi-all-0.0.23.tar.gz
% tar zxvf ruby-dbi-all-0.0.23.tar.gz
% cd ruby-dbi-all
% ruby setup.rb config --with=dbi,dbd_mysql
% ruby setup.rb setup
% ruby setup.rb install    # as superuser

Note

Ruby-DBI is a project in flux right now, and its documentation is scattered as a result. Check both the Rubyforge page and older SourceForge page if you need more info or help.

Install Lafcadio

... with RubyGems

Lafcadio has a number of external dependencies, and installing via RubyGems is highly recommended to help manage these dependencies. With RubyGems, you can install with

% gem install lafcadio     # as superuser

Note that MySQL/Ruby and Ruby-DBI are currently not Gem-installable, so you will have to install them by hand, as discussed above.

... without RubyGems

If for some reason you want to do an old fashioned install without RubyGems, you'll have to install its dependencies by yourself. After installing MySQL/Ruby and Ruby-DBI, you should also install the following Ruby libraries:

Once you've done that, you can install Lafcadio with

% ruby install.rb     # as superuser

Test Lafcadio (optional)

If you like, you can run all the tests. The test suite assumes you have a MySQL user with the username of "test" and a password of "password" (These values are set in lib/lafcadio/test/testconfig.dat.) You should create this user, and give it full permissions to a database called "test". You probably shouldn't let the account do anything more than that.

% cd lib
% ruby ../test/unitTests.rb
% ruby ../test/acceptanceTests.rb

Chapter 3. Tutorial

Create a configuration file

Configurations in Lafcadio are set in-Ruby, as opposed to an external file with YAML or XML. If you want to swap out different configurations, it's easy enough to do so by requiring different Ruby files.

For this tutorial we'll create a single Ruby file called tutorial_setup.rb. Here's what it should look like to start:

require     'rubygems'
require_gem 'lafcadio'

Lafcadio::LafcadioConfig.set_values(
  'dbuser' => 'test',
  'dbpassword' => 'password',
  'dbname' => 'test',
  'dbhost' => 'localhost'
)

Of course, if you're not using Rubygems you should just replace the first two lines with "require 'lafcadio'".

You can test this configuration with:

% ruby -e "require 'tutorial_setup'; Lafcadio::ObjectStore.get_object_store"

Create a table, and corresponding domain class

Object types that are represented in the database are called domain objects. You need to write a class definition for each different domain object type.

First, so we have a domain object type, let's create a User type. Log into MySQL and create a users table:

create table users (
  pk_id       int not null auto_increment,
  primary key (pk_id),
  first_name  varchar(64),
  last_name   varchar(64),
  email       varchar(64) not null,
  password    varchar(64) not null,
  birthday    date
);

Note that the primary key is named pk_id. Lafcadio is fairly flexible about what your tables look like, but it requires each table to have a numeric primary key. It assumes that it's going to be named pk_id, and that can be changed, but it's a little less work to go with the default.

Although we're creating a new table in this example, Lafcadio can also be integrated with pre-existing tables with only a little more work.

Next, write a child of DomainObject which defines the fields. Let's put this at the end of tutorial_setup.rb:

class User < Lafcadio::DomainObject
  string 'first_name'
  string 'last_name'
  string 'email'
  string 'password'
  date   'birthday'
end

You write one field directive for every field in the table, except for pk_id. For style reasons, Lafcadio expects the table name to be the plural (users) and expects the domain class type to be singular (User). You can override this assumption if you like.

Create, update, and delete Users

Now that we've defined a domain object class, we can write a script that makes use of this.

Create a new user and save it to the database

require 'tutorial_setup'

ten_years_ago = Date.today - (365 * 10)
john = User.new(
  'birthday' => ten_years_ago,
  'email' => 'john.doe@email.com',
  'first_name' => 'John',
  'last_name' => 'Doe',
  'password' => 'my_password'
)
john.commit

To create a user, you instantiate it with a hash. Notice that for the birthday field you don't have to think about SQL date formats; Lafcadio takes care of that automatically for you.

If you run this script you'll find that your users table will have one record in it with an pk_id 1; this is John Doe.

Retrieve a user and change some values

require 'tutorial_setup'

john = User[1]
ten_years = 365 * 10
john.birthday -= ten_years
john.email = 'john.doe@mail.email.com'
john.commit

Note that all the domain object properties can be accessed like any object's properties. Furthermore, when you committed the object last time, the object store inserted a row into the database. This time, it updates the pre-existing row instead.

Delete the user

require 'tutorial_setup'

john = User[1]
john.delete = true
john.commit

Add methods to the User class

The User class can be saved to the database, but it's also a normal Ruby class, too. Accordingly, you can add methods to it to give yourself functionality that might be a pain to get with a SQL statement.

For example, some users will enter only their first name or only their last name, or no name at all. It'd be convenient to have a method that accounts for this, so:

require 'tutorial_setup'

class User < Lafcadio::DomainObject
  def name; [ first_name, last_name ].compact.join( ' ' ); end
end

Record an association between Users and Messages

Since you're dealing with a relational database, you'll likely want to have different tables relating to one another. For example, let's say you want to have a simple email-like system where users can send one another messages.

First, create a messages table in MySQL:

create table messages (
  pk_id       int not null auto_increment,
  primary key (pk_id),
  subject     varchar(64) not null,
  body        varchar(255) not null,
  author      int not null,
  recipient   int not null,
  date_sent   date not null 
);

Then add a Message class to the end of tutorial_setup.rb.

class Message < Lafcadio::DomainObject
  string        'subject'
  string        'body'
  domain_object User, 'author'
  domain_object User, 'recipient'
  date          'date_sent'
end

As before, you leave out the pk_id field.

Once you've defined the Message class, you can treat instances of User as values of the fields Message#author and Message#recipient.

require 'tutorial_setup'

john = User[1]
five_years_ago = Date.today - ( 365 * 5 )
jane = User.new(
	'birthday' => five_years_ago
  'email' => 'jane.doe@email.com',
  'first_name' => 'Jane',
  'last_name' => 'Doe',
  'password' => 'jane_pass',
)
jane.commit
message_body = "Hey, Jane,\n\nWanna go to the movies on Saturday?"
message = Message.new(
  'subject' => 'hey',
  'body' => message_body,
  'author' => john,
  'recipient' => jane,
  'date_sent' => Date.today
)
message.commit

With this code Lafcadio will save all the appropriate id numbers in the right fields in MySQL.

Query Users

To query User, you use Lafcadio's query inference.

require 'tutorial_setup'

does = User.get { |u| u.last_name.equals( 'Doe' ) }
puts "There are #{ does.size } user(s) with the last name 'Doe'."
jane_does = User.get { |u|
  Lafcadio::Query.And(
    u.first_name.equals( 'Jane' ), u.last_name.equals( 'Doe' )
  )
}
puts "There are #{ jane_does.size } user(s) with the first name 'Jane' and the last name 'Doe'."
messages_to_jane = Message.get { |m| m.recipient.equals( jane_does.first ) }
puts "There are #{ messages_to_jane.size } message(s) to Jane Doe."

Chapter 4. Matching database tables to domain classes

Defining domain class fields

Lafcadio takes a Ruby-centric approach to object-relational mapping, as opposed to a database-centric approach. That means it relies on information in the Ruby code, as opposed to a live database schema, to tell it what fields tables have. The tradeoffs to this are significant. On one hand, Lafcadio has a test mode that runs in-memory, and in tandem with this test mode, defining domain classes fully in-Ruby means that you can write and test an application using Lafcadio without even having a database installed. On the other hand, there's definitely more typing up-front, as you have to define individual fields in Lafcadio classes.

Domain class fields are set with one-line class methods which look something like:

class Message < Lafcadio::DomainObject
  string        'subject'
  string        'body'
  domain_object User, 'author'
  domain_object User, 'recipient'
  date          'date_sent'
end

Note

Earlier versions of Lafcadio involved overriding DomainObject.get_class_fields and writing XML configuration files to determine fields. Such methods of specifying domain class fields are deprecated and may break in future releases.

When specifying fields, additional arguments can be passed this way:

class Message < Lafcadio::DomainObject
  string 'subject', { 'not_nil' => false }
  string 'body', { 'db_field_name' => 'b' }
end

The following arguments apply across all field types:

  • db_field_name: By default, fields are assumed to have the same name in the database, but you can override this assumption using db_field_name.
  • not_nil: This is true by default. Set it to false to avoid checking for nil values in tests. (For more on testing, see the "Testing" chapter below.)

Every field can be specified using the pluralized method as well:

class User < Lafcadio::DomainObject
	strings    'fname', 'lname'
	date_times 'created', 'modified'
end

Many of the fields map directly to common types of fields in SQL databases, but a few are also specialized extensions that might be convenient. The fields are:

binary

Maps to a binary field in the database. This acts fairly the same as string, below.

boolean

A field representing a boolean value. By default, it assumes that the table field represents true and false with the integers 1 and 0. To change this default, pass a hash with the keys true and false:

class User < Lafcadio::DomainObject
  boolean 'administrator', { 'enums' => { true => 'yin', false => 'yang' } }
end

date

A field representing a date.

date_time

A field representing a time.

domain_object

A field representing a relation to another domain object. To add such an association in a class definition, call DomainObject.domain_object:

class Invoice < Lafcadio::DomainObject
  domain_object Client
end

Note that the syntax domain_object differs from that of other field definitions, in that the argument passed is a domain class, not a field name. By default, the field name is assumed to be the same as the class name, only lower-cased and camel-case.

class LineItem < Lafcadio::DomainObject
  domain_object Product         # field name 'product'
  domain_object CatalogOrder    # field name 'catalog_order'
end

The field name can be explicitly set as the 2nd argument of DomainObject.domain_object.

class Message < Lafcadio::DomainObject
  domain_object User, 'sender'
  domain_object User, 'recipient'
end

Setting delete_cascade to true means that if the domain object being associated to is deleted, this domain object will also be deleted.

class Invoice < Lafcadio::DomainObject
  domain_object Client, 'client', { 'delete_cascade' => true }
end
cli = Client.new( 'name' => 'big company' ).commit
inv = Invoice.new( 'client' => cli ).commit
cli.delete!
inv_prime = Invoice[inv.pk_id] # => will raise DomainObjectNotFoundError

email

email takes a text value that is expected to be formatted as a single valid email address. It acts the same as text in production code, but if you're validating field types in tests, this will check to ensure it's a valid email address. See the "Testing" chapter for more on validating domain object fields.

enum

enum represents an enumerated field that can only be set to one of a set range of string values. To set the enumeration, pass in an Array of values as "enums". These enumerations are tested against during field validation.

class IceCream < Lafcadio::DomainObject
  enum 'flavor', { 'enums' => %w( Vanilla Chocolate Lychee ) }
end

float

float represents a Float value.

integer

integer represents an Integer value.

month

Accepts a Month as a value. (This convenience class is imported from the Ruby Month library.) Values of this field type will be saved in the database as the first day of the month.

state

A special enum field whose possible values are any of the 50 states of the United States, stored as each state's two-letter postal code.

string

Contains a String value.

text_list

Maps to any String SQL field that tries to represent a quick-and-dirty list with a comma-separated string. It returns an Array. For example, a SQL field with the value "john,bill,dave", then the Ruby field will have the value [ "john", "bill", "dave" ].

Naming assumptions, and how to override them

Lafcadio assumes that each table has a single primary key named pk_id. This can be overridden by using DomainObject.sql_primary_key_name.

class User < Lafcadio::DomainObject
  strings 'fname', 'lname'
  sql_primary_key_name 'user_id'
end

No matter what the primary key is named in the database, in your Ruby code you'll always call DomainObject#pk_id.

Similarly, Lafcadio assumes that the database name of the table is the same as the class name, only lower-case, pluralized, and with underscores instead of camel case.

class User < Lafcadio::DomainObject; end
puts User.table_name          # "users"
class BlogEntry < Lafcadio::DomainObject; end
puts BlogEntry.table_name     # "blog_entries"

You can override this behavior by calling DomainObject.table_name and passing it an argument.

class BlogEntry < Lafcadio::DomainObject
  table_name 'some_other_table'
end
puts BlogEntry.table_name     # "some_other_table"

Domain class inheritance

Inheritance can be achieved in Lafcadio by using separate tables, one for each level in a class hierarchy. For each record in the child table, there will also be one in the parent table as well. Retrievals will draw from both tables, and commits will affect both tables.

For example, let's imagine a web site with users, and administrators. Administrators are also users. Let's say the schemas look something like this:

create table users (
  pk_id       int not null auto_increment,
  primary key (pk_id),
  first_name  varchar(64),
  last_name   varchar(64),
  email       varchar(64) not null,
  password    varchar(64) not null,
  birthday    date
);

create table administrators (
  pk_id       int not null auto_increment,
  primary key (pk_id),
  granted_by  int,
  last_login  datetime
);

The matching Ruby code looks like this:

class User < Lafcadio::DomainObject
  string 'first_name'
  string 'last_name'
  string 'email'
  string 'password'
  date   'birthday'
end

class Administrator < User
  domain_object User, 'granted_by'
  date_time     'last_login'
end

Users and administrators can now be instantiated and retrieved naturally. Lafcadio handles the work of matching primary keys, and stitching together the results of multiple SQL statements.

user1 = User.new(
  'first_name' => 'John',
  'last_name' => 'Doe',
  'email' => 'john.doe@email.com'
).commit

admin2 = Administrator.new(
	'first_name' => 'Jane',
  'last_name' => 'Doe',
  'email' => 'jane.doe@email.com',
  'last_login' => Time.now
).commit

user1_pk_id = user1.pk_id     # most likely 1
admin2_pk_id = admin2.pk_id   # most likely 2
user1_prime = User[user1_pk_id]
admin2_prime = Administrator[admin2_pk_id]

One important caveat: Lafcadio does not correctly protect you from retrieving, as a parent instance, an instance that might more correctly be a child. For example, the following code will work in Lafcadio:

user2_prime = User[admin2_pk_id]

This code retrieves an instance of User, using a pk_id that could just as well be an Administrator, and it will contain only the methods and data of a User instance. Lafcadio will not detect that this is happening, so your program logic should not assume that just because it was possible to retrieve it as a User that it is not an Administrator.

Chapter 5. Using domain classes

Inserting, updating, and deleting

To insert a table row with Lafcadio, simply create a domain object in Ruby, then commit it. A newly created domain object has no pk_id set, but this will be set once it's been committed.

user = User.new( 'first_name' => 'John', 'last_name' => 'Doe' )
puts user.pk_id     # nil
user.commit
puts user.pk_id     # won't be nil

Unless you're writing test code and using the MockObjectStore, it's not recommended to create a domain object with a pk_id. Doing so will override the database's role in setting the primary key for you, and can have unpredictable results.

# Don't do this:
user = User.new( 'pk_id' => 99, 'first_name' => 'John', 'last_name' => 'Doe' )
user.commit

To update an existing domain object, you retrieve it with DomainObject[], modify some fields, and then commit it.

user = User[5555]
user.last_name = 'Smith'
user.commit

You can shortcut this with DomainObject#update!, with takes a hash of values to update, and then commits the domain object.

user.update!( 'last_name' => 'Smith' )

Deleting a domain object simply involves setting DomainObject.delete to true, and then committing it.

user.delete = true
user.commit

Dynamic associations

Using a domain_object field will automatically create an associations method on the domain class being associated to. For example:

class Client < Lafcadio::DomainObject
  string 'client_name'
end

class Invoice < Lafcadio::DomainObject
  date          'date'
  float         'amount'
  domain_object Client
end

...

client = Client[123]
invoices = client.invoices
invoices.each do |inv|
  puts "invoice #{ inv.pk_id } is for $#{ inv.amount }"
end

Triggers

You can define triggers as methods by defining pre_commit_trigger and post_commit_trigger, which run before or after the commit, respectively.

class Payment < Lafcadio::DomainObject
  domain_object User
  float         'amount'

  def post_commit_trigger
    self.user.email_thanks_for_your_payment_letter
  end
end

You can use triggers to check values and raise exceptions, if you like.

class Payment < Lafcadio::DomainObject
  domain_object User
  float         'amount'

  def pre_commit_trigger
    raise if amount < 0
  end
end

Each domain object automatically maintains a hash of values called @original_values, which records the last values known to come from the database. So if the domain object has been withdrawn from the database, @original_values will contain the values from the database regardless of changes made to the Ruby version of the domain object. When you commit to the database, @original_values is reset.

class Payment < Lafcadio::DomainObject
  domain_object User
  float         'amount'

  def pre_commit_trigger
    raise if amount != @original_values['amount']
  end
end

Chapter 6. Querying

When using Lafcadio, you can pass in a block to DomainObject.get in order to write complex, ad-hoc queries in Ruby. This involves a few more keystrokes than writing raw SQL, but also makes it easier to change queries at runtime, and these queries can also be fully tested against the MockObjectStore.

big_invoices = Invoice.get { |inv| inv.rate.gt( 50 ) }
# => runs "select * from invoices where rate > 50"

This a full-fledged block, so you can pass in values from the calling context.

date = Date.new( 2004, 1, 1 )
recent_invoices = Invoice.get { |inv| inv.date.gt( date ) }
# => runs "select * from invoices where date > '2004-01-01'"

Building and accessing queries

Most commonly, you'll access the query language through DomainObject.get, which builds a query and runs it immediately.

# runs "select * from users where first_name = 'Jane'"
janes = User.get { |u| u.first_name.equals( 'Jane' ) }

If you want more fine-grained control over a query, first create it with Query.infer and then build it, using ObjectStore#query to run it. This can come in handy if you're doing complex query construction, for example if you're dealing with an advanced search with many possible search options.

qry = Query.infer( User ) { |u| u.last_name.equals( 'Hwang' ) }
qry.to_sql # => "select * from users where users.last_name = 'Hwang'"
qry = qry.and { |u| u.first_name.equals( 'Francis' ) }
qry.to_sql
# => "select * from users where (users.last_name = 'Hwang' and users.first_name = 'Francis')"
qry.limit = 0..5
qry.to_sql
# => "select * from users where (users.last_name = 'Hwang' and users.first_name = 'Francis') limit 0, 6"

Using Query.infer, you can also set order_by and order_by_order clauses.

qry = Query.infer(
  SKU,
  :order_by => [ :standardPrice, :salePrice ],
  :order_by_order => :desc
) { |s| s.sku.nil? }
qry.to_sql
# => "select * from skus where skus.sku is null order by standardPrice, salePrice desc"

Query inference operators

You can compare fields either to simple values, or to other fields in the same table.

paid_immediately = Invoice.get { |inv|
  inv.date.equals( inv.paid )
}
# => runs "select * from invoices where date = paid"

Numerical comparisons: lt, lte, gte, gt

lt, lte, gte, gt stand for "less than", "less than or equal", "greater than or equal", and "greater than", respectively.

tiny_invoices = Invoice.get { |inv| inv.rate.lte( 25 ) }
# => runs "select * from invoices where rate <= 25"

These comparators work on fields that contain numbers, dates, and even references to other domain objects.

for_1st_ten_clients = Invoice.get { |inv|
  inv.client.lte( 10 )
}
# => runs "select * from invoices where client <= 10"
client10 = Client[10]
for_1st_ten_clients = Invoice.get { |inv|
  inv.client.lte( client10 )
}
# => runs "select * from invoices where client <= 10"

Equality: equals

full_week_invs = Invoice.get { |inv| inv.hours.equals( 40 ) }
# => "select * from invoices where hours = 40"

If you're comparing to a domain object you should pass in the object itself.

client = Client[99]
invoices = Invoice.get { |inv| inv.client.equals( client ) }
# => "select * from invoices where client = 99"

If you're comparing to a boolean value you don't need to use equals( true ).

administrators = User.get { |u| u.administrator.equals( true ) }
administrators = User.get { |u| u.administrator } # both forms work

Matching for nil can use nil?

no_email = User.get { |u| u.email.nil? }

Inclusion: in and include?

Any field can be matched via in:

first_three_invs = Invoice.get { |inv| inv.pk_id.in( 1, 2, 3 ) }
# => "select * from invoices where pk_id in ( 1, 2, 3 )"

A TextListField can be matched via include?

aim_users = User.get { |u| u.im_methods.include?( 'aim' ) }
# => "select * from users where user.im_methods like 'aim,%' or
#     user.im_methods like '%,aim,%' or user.im_methods like '%,aim' or
#     user.im_methods = 'aim'"

Text comparison: like

fname_starts_with_a = User.get { |user| user.fname.like( /^a/ ) }
# => "select * from users where fname like 'a%'"
fname_ends_with_a = User.get { |user| user.fname.like( /a$/ ) }
# => "select * from users where fname like '%a'"
fname_contains_a = User.get { |user| user.fname.like( /a/ ) }
# => "select * from users where fname like '%a%'"
james_or_jones = User.get { |user| user.lname.like( /J..es/ ) }
# => "select * from users where lname like 'J__es'"

Please note that although we're using the Regexp operators here, these aren't full-fledged regexps. Only ^, $, and . work for this.

Compound conditions: & and |

invoices = Invoice.get { |inv|
  inv.hours.equals( 40 ) & inv.rate.equals( 50 )
}
# => "select * from invoices where (hours = 40 and rate = 50)"
client99 = Client[99]
invoices = Invoice.get { |inv|
  inv.hours.equals( 40 ) | inv.rate.equals( 50 ) |
    inv.client.equals( client99 )
}
# => "select * from invoices where (hours = 40 or rate = 50 or client = 99)"

Note that both compound operators can be nested:

invoices = Invoice.get { |inv|
  inv.hours.equals( 40 ) &
    ( inv.rate.equals( 50 ) | inv.client.equals( client99 ) )
}
# => "select * from invoices where (hours = 40 and 
#     (rate = 50 or client = 99))"

Negation: not

invoices = Invoice.get { |inv| inv.rate.equals( 50 ).not }
# => "select * from invoices where rate != 50"

This can be used directly against boolean and nil comparisons, too.

not_administrators = User.get { |u| u.administrator.not }
# => "select * from users where administrator != 1"
has_email = User.get { |u| u.email.nil?.not }
# => "select * from users where email is not null"

Hands-on query construction

The standard way to run a query is through DomainObject.get, which generates the query and runs it immediately, returning the results. However, there are times when you'll want a more fine-grained way to construct a query.

As an example, let's imagine a search form on a online dating website. The code to generate the query will use Query#infer, and then run it via ObjectStore#query.

qry = Lafcadio::Query.infer( User ) { |u|
  u.gender.equals( @search[:gender] )
}
if @search[:min_age]
  qry = qry.and { |u| u.age.gte( @search[:min_age] }
end
if @search[:max_age]
  qry = qry.and { |u| u.age.lte( @search[:max_age] }
end
if @search[:require_photos]
  qry = qry.and { |u| u.num_photos.gte( 1 ) }
end
matches = Lafcadio::ObjectStore.get_object_store.query( qry )

Query caching via subset matching

Lafcadio caches every query, and optimizes based on a simple subset calculation. For example, if you run these statements:

User.get { |u| u.lname.equals( 'Smith' ) }
User.get { |u| u.lname.equals( 'Smith' ) & u.fname.like( /John/ ) }
User.get { |u| u.lname.equals( 'Smith' ) & u.email.like( /hotmail/ ) }

Lafcadio can tell that the 2nd and 3rd queries are subsets of the first. So these three statements will result in one database call, for the first statement: The 2nd and 3rd statements will be handled entirely in Ruby. The result is less database calls with no extra work for the programmer.

Eager loading

As of Lafcadio 0.9.3, you can use eager loading to pre-join requested domain objects. This reduces the number of SQL calls.

invoices_and_clients = Invoice.all( :include => :client )
# => "select * from invoices left outer join clients on invoices.client = clients.pk_id"

The :include argument works through DomainObject convenience methods such as DomainObject.all and DomainObject.get. It can also be used while creating a query by hand with Query.new.

iac_query = Query.new( Invoice, :include => :client )
# => "select * from invoices left outer join clients on invoices.client = clients.pk_id"

Chapter 7. Testing

Setting up testing in a Lafcadio-based program

Lafcadio's philosophy about testing is to try to mock out as much as possible, so that extensive database-dependent tests can be run through memory quickly. The primary engine behind that is the MockObjectStore, which pretends to be the ObjectStore representing the database.

mock_object_store = MockObjectStore.new
ObjectStore.set_object_store mock_object_store

Adding the above two lines to your test class' setup method will make every database-dependent call going through Lafcadio go through the virtual database.

Domain objects in test cases

Once your MockObjectStore is setup, domain objects in test cases can be setup the same as they would be in live code.

def test_commit_user_msg
  john = User.new( 'first_name' => 'John', 'last_name' => 'Doe' ).commit
  jane = User.new( 'first_name' => 'John', 'last_name' => 'Doe' ).commit
  commit_user_msg( john, jane, "Hi Jane, let's go to the movies" )
  msg = Message.all.first
  assert_equal( john, msg.author )
end

One small difference is that in live code, you should never set pk_id for a new domain object, because it's the database's job to assign new primary keys. In test code running through the MockObjectStore, you can set the pk_id if you want to.

def test_msg_url
  msg = Message.new( 'pk_id' => 999 )
  assert_equal( "/msg/999", msg.url )
end

Quicker ways to setup mock objects

Setting up mock objects can be somewhat tedious, so you can add the methods DomainObject.default_mock and DomainObject.custom_mock by requiring lafcadio/test.rb.

DomainObject.default_mock will commit its best guess of a typical instance of the class if one doesn't already exist, and then return that instance. Field values will be valid, but for the most part meaningless. This instance will always have a pk_id of 1, and any relations to other domain classes will refer to the default_mock of that class. Note that multiple calls to DomainObject.default_mock will return the same instance.

require 'lafcadio/test'
mock_object_store = MockObjectStore.new
ObjectStore.set_object_store mock_object_store
john = User.default_mock
puts john.first_name        # "test text"
puts john.email             # "test text"
john2 = User.default_mock
puts ( john == john2 )      # true
msg = Message.default_mock
puts ( msg.author == john ) # true

If you want to create custom mocks that are mostly the same but vary in a few fields, you can use DomainObject.custom_mock. This always commits a new instance, with specific fields set to new values.

jane = User.custom_mock( 'first_name' => 'Jane' )
puts ( john == jane )       # false

The field values that Lafcadio guesses are really pretty stupid, and are just there to be a half-step better than nil. You might want to create better values for those mock object fields, and you can do so with DomainObject.mock_values. These will affect the default values used both in DomainObject.default_mock and DomainObject.custom_mock. Note that given the open nature of Ruby classes, you can have this mock_values code in an entirely separate file, to only be required for testing.

class User < Lafcadio::DomainObject
  mock_values 'first_name' => 'John', 'last_name' => 'Doe',
              'email' => 'john.doe@email.com', 'password' => 'password',
              'birthday' => ( Date.today - 365 * 30 )
end
john = User.default_mock
puts john.first_name        # "John"
puts john.last_name         # "Doe"
puts john.email             # "john.doe@email.com"
jane = User.custom_mock(
  'first_name' => 'Jane', 'email' => 'jane.doe@email.com'
)
puts jane.first_name        # "Jane"
puts jane.last_name         # "Doe"
puts jane.email             # "jane.doe@email.com"

And if your test case includes Lafcadio::DomainMock, you can use the class method setup_mock_dobjs to auto-create default mocks as part of the setup method. These will be set as local variables. Note that if you write your own setup method in such a test case, you should remember to call super to ensure that the mock setup is run correctly.

class TestUser < Test::Unit::TestCase
  include Lafcadio::DomainMock
  setup_mock_dobjs User

  def test_first_name
    assert_equal( "John", @user.first_name )
  end
end

Chapter 8. Advanced ObjectStore tricks

Transactions

Lafcadio will support transactions as long as the underlying table type will support them. This is done using blocks; when the block is exited, the transaction is committed. For example, in a PayPal-like system where you're transferring money from one user to another, you want transactions to make sure all the rows are recorded or none of them are.

object_store = Lafcadio::ObjectStore.get_object_store
object_store.transaction do |tr|
  from_account.update!( 'amount' => from_account.amount - transfer_amount )
  to_account.update!( 'amount' => to_account.amount + transfer_amount )
end

Rollbacks work by calling the rollback method on the transactional object passed through the block.

object_store.transaction do |tr|
  from_account.update!( 'amount' => from_account.amount - transfer_amount )
  tr.rollback
  to_account.update!( 'amount' => to_account.amount + transfer_amount )
end

In fact, raising any sort of error within the transaction will rollback the transaction.

object_store.transaction do |tr|
  from_account.update!( 'amount' => from_account.amount - transfer_amount )
  raise "just kidding"
  to_account.update!( 'amount' => to_account.amount + transfer_amount )
end

Note also that the MockObjectStore handles transactions as well. This means that you can write unit-tests testing transaction-dependent logic, and test them in-memory.

The ObjectStore cache

ObjectStore automatically caches the results of any selects it runs. This reduces SQL calls in surprising ways, in particular by calculating if queries are subsets of previous queries. The cache is only updated on updates, inserts, and deletes.

If you are running a long-running program that does a lot of selects, without updates, inserts, and deletes--for example, a big reporting program--you'll find that the program will use a lot of memory, as it slowly begins to clone the database in RAM. You can trim the cache by calling ObjectStore#flush.

object_store.flush( user )

Chapter 9. Credits

Lafcadio is written and maintained by Francis Hwang.

Thanks to Kaspar Schiess for submitting patches for added Binary field support, and fixing problems with inserts on inherited domain objects.