RDO—Database Connectivity for Ruby
RDO provides a simple, robust standardized way to access various RDBMS
implementations in Ruby. Drivers all conform to the same, beautiful rubyesque
interface. Where a feature is not natively supported by the DBMS—perhaps
prepared statements—it is seamlessly emulated, so you don't need to code
around it.
It targets Ruby 1.9 and newer (including Rubinius 2.0).
RDO stands for Ruby Data Objects.
require "rdo"
require "rdo-postgres"
conn = RDO.connect("postgres://user:pass@localhost/dbname?encoding=utf-8")
result = conn.execute(
"INSERT INTO users (
name, password_hash, created_at, updated_at
) VALUES (?, ?, ?, ?) RETURNING id",
'bob',
Digest::MD5.hexdigest('secret'),
Time.now,
Time.now
)
puts "Inserted user ID = #{result.insert_id}"
result = conn.execute("SELECT * FROM users WHERE name LIKE ?", "%jim%")
result.each do |row|
puts "#{row[:id]}: #{row[:name]}"
end
conn.close
Features
RDO makes the following things standard:
- Consistent class/method contracts for all drivers
- Native bind parameters where possible; emulated where not
- Prepared statements where possible; emulated where not
- Type-casting to equivalent Ruby types (e.g. Fixnum, BigDecimal,
Float, even Array)
- Access result information after write operations, with insert IDs standardized
- Use simple core data types (Hash) for reading values and field names
Note that data-type support is limited to whatever the DBMS actually supports.
See individual driver READMEs for type support information.
Installation
RDO doesn't do anything by itself. You need to also install the driver for
your DBMS. Install via Rubygems.
$ gem install rdo
$ gem install rdo-postgres
Or add a line to your application's Gemfile:
gem "rdo"
gem "rdo-postgres"
And then execute:
$ bundle
Available Drivers
I'm looking for contributors to develop and maintain drivers for other vendors:
Oracle, SQL Server and DB2 are of interest. Your project would be linked above.
Usage
The interface for RDO is intentionally minimal. It should take a few minutes
to learn just about everything.
Connecting to a database
A connection is established when you initialize an RDO::Connection. The
easiest way to do that is through RDO.connect
. Make sure you have required
the driver for RDO first, or it will explode, like, all in your face and stuff.
require "rdo"
require "rdo-postgres"
conn = RDO.connect("postgresql://user:pass@host:port/db_name?encoding=utf-8")
p conn.open?
For semantic reasons, #connect is aliased to #open.
If it is not possible to establish a connection an RDO::Exception is raised,
which should provide any reason given by the DBMS.
You can also pass a block to #connect. This has the same semantics as passing
a block to File#open (i.e. it passes itself to the block, returns the value
of the block and finally closes the connection).
Disconnecting
RDO will disconnect automatically when the connection is garbage-collected,
or when the program exits, but if you need to disconnect explicitly,
call #close. It is safe to call this even if the connection is already closed.
If you have called #close, say before forking, call #open to re-connect.
conn.close
p conn.open?
conn.open
p conn.open?
Performing non-read commands
Any command supported by the DBMS is executed with #execute, which always
returns a RDO::Result object. Query inputs should be provided as binding
placeholders and additional arguments. No explicit type-conversion is
necessary.
result = conn.execute("CREATE TABLE bob ( ... )")
result = conn.execute("UPDATE users SET banned = ?", true)
p result.affected_rows
result = conn.execute(
"INSERT INTO users (name, created_at) VALUES (?, ?) RETURNING id",
"Jimbo Baggins",
Time.now
)
p result.insert_id
p result.execution_time
result.each do |row|
p row[:id]
end
In the event of a query error, an RDO::Exception is raised, which should
include any error messaage provided by the DBMS.
Performing read queries
There is no difference in the interface for reads or writes. Just call
the #execute method in both cases. This always returns an RDO::Result,
which includes the Enumerable module. Some operations, such as #count
may be optimized by the driver.
result = conn.execute("SELECT id, name FROM users WHERE created_at > ?", 1.week.ago)
p result.count
result.each do |row|
p "#{row[:id]}: #{row[:name]}"
end
In the event of a query error, an RDO::Exception is raised, which should
include any error messaage provided by the DBMS.
Using prepared statements
Most mainstream databases support them. Some don't, but RDO emulates them in
that case. Prepared statements provide safety through bind parameters and
efficiency through query re-use, because the query planner only executes once.
Prepare a statement with #prepare, then execute it with #execute, passing in
any bind parameters. An RDO::Result is returned.
stmt = conn.prepare("SELECT * FROM users WHERE name LIKE ? AND banned = ?")
%w[bob jim harry].each do |name|
result = stmt.execute("%#{name}%", false)
result.each do |row|
p "#{row[:id]: row[:name]}"
end
end
RDO simply delegates to #execute if the driver doesn't support prepared
statements.
In the event of a query error, an RDO::Exception is raised, which should
include any error messaage provided by the DBMS.
Tread carefully, there be danger ahead
While driver developers are expected to provide a suitable implementation,
it is generally riskier to escape and interpolate inputs directly into the
SQL than it is to use bind parameters. There are times where you might
need to escape some input yourself, however. For that, you can call #quote.
conn.execute("INSERT INTO users (name) VALUES ('#{conn.quote(params[:name])}')")
Column names with whitespace in them
RDO uses Symbols as keys in the hashes that represent data rows. Most of the
time this is desirable. If you query for something that returns field names
containing spaces, or punctuation, you need to convert a String to a Symbol
using #to_sym or #intern. Or use a quoted Symbol-literal.
result = conn(%q{SELECT 42 AS "The Meaning"})
p result.first[:"The Meaning"]
p result.first["The Meaning".intern]
Selecting just a single value
RDO::Result has a #first_value method for convenience if you are only
selecting one row and one column.
p conn.execute("SELECT count(true) FROM users").first_value
This method returns nil if there are no rows, so if you need to distinguish
between NULL and no rows, you will need to check the result contents the
longer way around.
Disambiguating bind markers from operators
Some drivers use '?' for operators. In order to avoid ambiguity, escape such
occurrences with a backslash. You do not need to escape inside of strings
and comments (i.e. wherever a bind marker could not naturally occur).
conn.execute(%q{SELECT 'a=>42,b=>7'::hstore \? ?}, "a")
Debugging
A Logger instance (with an interface like that in Ruby stdlib) may be passed
in the options when creating a connection. All queries will be logged with
DEBUG severity. Errors will be logged with FATAL severity.
RDO.connect("postgres://user:pass@host/db", logger: Logger.new(STDOUT))
You can access the logger through connection.logger
.
conn.logger.level = Logger::DEBUG
conn.logger.debug?
A logger with some support for highlighting errors etc and which shows
query execution times is configured (but disabled) by default. It is
found at RDO::ColoredLogger
. You can enable it by specify a log level:
RDO.connect("postgres://user:pass@host/db", log_level: Logger::DEBUG)
If you want the log output to go somewhere other than stdout, instantiate
the logger manually.
Temporary debug output
Turning on debug logging globally is often a little overkill and too noisy.
You may enable debug logging in the context of a block, like so:
conn.debug do
end
The log level will be restored after the block has executed, even if an
Exception is raised.
Contributing
If you find a bug in RDO, send a pull request if you think you can fix it.
Your contribution will be recognized here. If you don't know how to fix it,
file an issue in the issue tracker on GitHub.
When sending pull requests, please use topic branches—don't send a pull
request from the master branch of your fork.
I haven't looked at what I need to change to have the drivers compile on
Windows yet, but I will do. If anybody beats me to it, pull requests will
be gladly accepted! I was going to write JDBC wrappers for JRuby, but have
decided to just aim for JRuby >= 1.6, which supports C extensions. This
hasn't yet been tested with RDO. I should be able to make it work, as the
parts of the Ruby API I use are very typical.
Writing a driver for RDO
The more drivers that RDO has support for, the better. Writing drivers for
RDO is quite painless. They are just thin wrappers around the C API for the
DBMS, which conform to RDO's Driver interface.
RDO::Driver
- open
- open?
- close
- execute
- prepare
- quote
The #execute method returns an RDO::Result, which takes any Enumerable and
some options in its initializer. The Enumerable just iterates over the rows
in the result. The options Hash provides result information.
The #prepare method is optional, but should return an Object with the
following methods:
RDO::StatementExecutor
- command
- execute
The #command method just provides the String form of the statement. The #execute
method returns an RDO::Result, as per the Driver.
Some of the more boilerplate things you'd normally have to do are covered by
C macros in the util/macros.h file you'll find in this repository. Copy that
file to your own project and include it for one-line type conversions etc.
Take a look at one of the existing drivers to get an idea how to write a
driver (rdo-sqlite and rdo-mysql are probably simple ones).
Copyright & Licensing
Written and maintained by Chris Corbyn.
Licensed under the MIT license. That pretty much means it's fair game to use
RDO as you please, but you should refer to the LICENSE file for details.