Swift
Description
A rational rudimentary object relational mapper.
Dependencies
- MRI Ruby >= 1.9.1
- swift-db-sqlite3 or swift-db-postgres or swift-db-mysql
Installation
Dependencies
Install one of the following drivers you would like to use.
gem install swift-db-sqlite3
gem install swift-db-postgres
gem install swift-db-mysql
Install Swift
gem install swift
Features
- Multiple databases.
- Prepared statements.
- Bind values.
- Transactions and named save points.
- Asynchronous API for PostgreSQL and MySQL.
- IdentityMap.
- Migrations.
DB
require 'swift'
require 'swift/adapter/postgres'
Swift.trace true # Debugging.
Swift.setup :default, Swift::Adapter::Postgres, db: 'swift'
# Block form db context.
Swift.db do |db|
db.execute('drop table if exists users')
db.execute('create table users(id serial, name text, email text)')
# Save points are supported.
db.transaction :named_save_point do
st = db.prepare('insert into users (name, email) values (?, ?) returning id')
puts st.execute('Apple Arthurton', 'apple@arthurton.local').insert_id
puts st.execute('Benny Arthurton', 'benny@arthurton.local').insert_id
end
# Block result iteration.
db.prepare('select * from users').execute do |row|
puts row.inspect
end
# Enumerable.
result = db.prepare('select * from users where name like ?').execute('Benny%')
puts result.first
end
DB Record Operations
Rudimentary object mapping. Provides a definition to the db methods for prepared (and cached) statements plus native primitive Ruby type conversion.
require 'swift'
require 'swift/adapter/postgres'
require 'swift/migrations'
Swift.trace true # Debugging.
Swift.setup :default, Swift::Adapter::Postgres, db: 'swift'
class User < Swift::Record
store :users
attribute :id, Swift::Type::Integer, serial: true, key: true
attribute :name, Swift::Type::String
attribute :email, Swift::Type::String
attribute :updated_at, Swift::Type::DateTime
end # User
Swift.db do |db|
db.migrate! User
# Select Record instance (relation) instead of Hash.
users = db.prepare(User, 'select * from users limit 1').execute
# Make a change and update.
users.each{|user| user.updated_at = Time.now}
db.update(User, *users)
# Get a specific user by id.
user = db.get(User, id: 1)
puts user.name, user.email
end
Record CRUD
Record/relation level helpers.
require 'swift'
require 'swift/adapter/postgres'
require 'swift/migrations'
Swift.trace true # Debugging.
Swift.setup :default, Swift::Adapter::Postgres, db: 'swift'
class User < Swift::Record
store :users
attribute :id, Swift::Type::Integer, serial: true, key: true
attribute :name, Swift::Type::String
attribute :email, Swift::Type::String
end # User
# Migrate it.
User.migrate!
# Create
User.create name: 'Apple Arthurton', email: 'apple@arthurton.local' # => User
# Get by key.
user = User.get id: 1
# Alter attribute and update in one.
user.update name: 'Jimmy Arthurton'
# Alter attributes and update.
user.name = 'Apple Arthurton'
user.update
# Destroy
user.delete
Conditions SQL syntax.
SQL is easy and most people know it so Swift ORM provides simple #to_s attribute to table and field name typecasting.
class User < Swift::Record
store :users
attribute :id, Swift::Type::Integer, serial: true, key: true
attribute :age, Swift::Type::Integer, field: 'ega'
attribute :name, Swift::Type::String, field: 'eman'
attribute :email, Swift::Type::String, field: 'liame'
end # User
# Convert :name and :age to fields.
# select * from users where eman like '%Arthurton' and ega > 20
users = User.execute(
%Q{select * from #{User} where #{User.name} like ? and #{User.age} > ?},
'%Arthurton', 20
)
Identity Map
Swift comes with a simple identity map. Just require it after you load swift.
require 'swift'
require 'swift/adapter/postgres'
require 'swift/identity_map'
require 'swift/migrations'
Swift.setup :default, Swift::Adapter::Postgres, db: 'swift'
class User < Swift::Record
store :users
attribute :id, Swift::Type::Integer, serial: true, key: true
attribute :age, Swift::Type::Integer, field: 'ega'
attribute :name, Swift::Type::String, field: 'eman'
attribute :email, Swift::Type::String, field: 'liame'
end # User
# Migrate it.
User.migrate!
# Create
User.create name: 'James Arthurton', email: 'james@arthurton.local' # => User
find_user = User.prepare(%Q{select * from #{User} where #{User.name = ?})
find_user.execute('James Arthurton')
find_user.execute('James Arthurton') # Gets same object reference
Bulk inserts
Swift comes with adapter level support for bulk inserts for MySQL and PostgreSQL. This is usually very fast (~5-10x faster) than regular prepared insert statements for larger sets of data.
MySQL adapter - Overrides the MySQL C API and implements its own infile handlers. This means currently you cannot execute the following SQL using Swift
LOAD DATA LOCAL INFILE '/tmp/users.tab' INTO TABLE users;
But you can do it almost as fast in ruby,
require 'swift'
require 'swift/adapter/mysql'
Swift.setup :default, Swift::Adapter::Mysql, db: 'swift'
# MySQL packet size is the usual limit, 8k is the packet size by default.
Swift.db do |db|
File.open('/tmp/users.tab') do |file|
count = db.write('users', %w{name email balance}, file)
end
end
You are not just limited to files - you can stream data from anywhere into your database without creating temporary files.
Asynchronous API
Swift::Adapter::Sql#query
runs a query asynchronously. You can either poll the corresponding
Swift::Adapter::Sql#fileno
and then call Swift::Adapter::Sql#result
when ready or use a block form like below
which implicitly uses rb_thread_wait_fd
require 'swift'
require 'swift/adapter/postgres'
pool = 3.times.map.with_index {|n| Swift.setup n, Swift::Adapter::Postgres, db: 'swift' }
Thread.new do
pool[0].query('select pg_sleep(3), 1 as qid') {|row| p row}
end
Thread.new do
pool[1].query('select pg_sleep(2), 2 as qid') {|row| p row}
end
Thread.new do
pool[2].query('select pg_sleep(1), 3 as qid') {|row| p row}
end
Thread.list.reject {|thread| Thread.current == thread}.each(&:join)
or use the swift/eventmachine
api.
require 'swift'
require 'swift/adapter/em/postgres'
EM.run do
pool = 3.times.map { Swift.setup(:default, Swift::Adapter::EM::Postgres, db: "swift") }
3.times.each do |n|
defer = pool[n].execute("select pg_sleep(3 - #{n}), #{n + 1} as qid")
defer.callback do |res|
p res.first
end
defer.errback do |e|
p 'error', e
end
end
end
or use the em-synchrony
api for swift
require 'swift'
require 'swift/adapter/synchrony/postgres'
EM.run do
3.times.each do |n|
EM.synchrony do
db = Swift.setup(:default, Swift::Adapter::Synchrony::Postgres, db: "swift")
result = db.execute("select pg_sleep(3 - #{n}), #{n + 1} as qid")
p result.first
EM.stop if n == 0
end
end
end
Fibers and Connection Pools
If you intend to use Swift::Record
with em-synchrony
you will need to use a fiber aware connection pool.
require 'swift/fiber_connection_pool'
EM.run do
Swift.setup(:default) do
Swift::FiberConnectionPool.new(size: 5) {Swift::Adapter::Synchrony::Postgres.new(db: 'swift')}
end
5.times do
EM.synchrony do
p User.execute("select * from users").entries
end
end
end
Performance
Swift prefers performance when it doesn't compromise the Ruby-ish interface. It's unfair to compare Swift to DataMapper and ActiveRecord which suffer under the weight of support for many more databases and legacy/alternative Ruby implementations. That said obviously if Swift were slower it would be redundant so benchmark code does exist in http://github.com/shanna/swift/tree/master/benchmarks
Benchmarks
ORM
The test environment:
$ uname -a
Linux deepfryed.local 3.0.0-1-amd64 #1 SMP Sun Jul 24 02:24:44 UTC 2011 x86_64 GNU/Linux
$ cat /proc/cpuinfo | grep "processor\|model name"
processor : 0
model name : Intel(R) Core(TM) i7-2677M CPU @ 1.80GHz
processor : 1
model name : Intel(R) Core(TM) i7-2677M CPU @ 1.80GHz
processor : 2
model name : Intel(R) Core(TM) i7-2677M CPU @ 1.80GHz
processor : 3
model name : Intel(R) Core(TM) i7-2677M CPU @ 1.80GHz
$ ruby -v
ruby 1.9.3p125 (2012-02-16 revision 34643) [x86_64-linux]
PostgreSQL config:
shared_buffers = 800MB # min 128kB
effective_cache_size = 512MB
work_mem = 64MB # min 64kB
maintenance_work_mem = 64MB # min 1MB
The test setup:
- 10,000 rows are created once.
- All the rows are selected once.
- All the rows are selected once and updated once.
- Memory footprint(rss) shows how much memory the benchmark used with GC disabled. This gives an idea of total memory use and indirectly an idea of the number of objects allocated and the pressure on Ruby GC if it were running. When GC is enabled, the actual memory consumption might be much lower than the numbers below.
./simple.rb -n1 -r10000 -s ar -s dm -s sequel -s swift
benchmark sys user total real rss
ar #create 1.960000 15.81000 17.770000 22.753109 266.21m
ar #select 0.020000 0.38000 0.400000 0.433041 50.82m
ar #update 2.000000 17.90000 19.900000 26.674921 317.48m
dm #create 0.660000 11.55000 12.210000 15.592424 236.86m
dm #select 0.030000 1.30000 1.330000 1.351911 87.18m
dm #update 0.950000 17.25000 18.200000 22.109859 474.81m
sequel #create 1.960000 14.48000 16.440000 23.004864 226.68m
sequel #select 0.000000 0.09000 0.090000 0.134619 12.77m
sequel #update 1.900000 14.37000 16.270000 22.945636 200.20m
swift #create 0.520000 1.95000 2.470000 5.828846 75.26m
swift #select 0.010000 0.070000 0.080000 0.095124 11.23m
swift #update 0.440000 1.95000 2.390000 6.044971 59.35m
swift #write 0.010000 0.050000 0.060000 0.117195 13.46m
TODO
- More tests.
- Assertions for dumb stuff.
- Auto-generate schema?
- Move examples to Wiki. Examples of models built on top of Schema.
Contributing
Go nuts! There is no style guide and I do not care if you write tests or comment code. If you write something neat just send a pull request, tweet, email or yell it at me line by line in person.