Swift
Description
A rational rudimentary object relational mapper.
Dependencies
-
ruby >= 1.9.1
-
dbic++ >= 0.4.0 (github.com/deepfryed/dbicpp)
-
mysql >= 5.0.17 or postgresql >= 8.4 or db2 >= 9.7.2
Caveats
DB2
-
The server needs to be running under DB2_COMPATIBILITY_VECTOR=77FF if you want to use the ORM features of swift.
-
DB2 asynchronous operations are highly experimental at this point due to inherent limitations of the underlying api. It is more an academic exercise and is not ready for real-world use.
Features
-
Multiple databases.
-
Prepared statements.
-
Bind values.
-
Transactions and named save points.
-
EventMachine asynchronous interface.
-
IdentityMap.
-
Migrations.
Synopsis
DB
require 'swift'
Swift.trace true # Debugging.
Swift.setup :default, Swift::DB::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', '[email protected]').insert_id
puts st.execute('Benny Arthurton', '[email protected]').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 Scheme 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/migrations'
Swift.trace true # Debugging.
Swift.setup :default, Swift::DB::Postgres, db: 'swift'
class User < Swift::Scheme
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::Time
end # User
Swift.db do |db|
db.migrate! User
# Select Scheme 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
Scheme CRUD
Scheme/relation level helpers.
require 'swift'
require 'swift/migrations'
Swift.trace true # Debugging.
Swift.setup :default, Swift::DB::Postgres, db: 'swift'
class User < Swift::Scheme
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: '[email protected]' # => 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.destroy
Conditions SQL syntax.
SQL is easy and most people know it so Swift ORM provides a simple symbol like syntax to convert resource names to field names.
class User < Swift::Scheme
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.all(':name like ? and :age > ?', '%Arthurton', 20)
Identity Map
Swift comes with a simple identity map. Just require it after you load swift.
require 'swift'
require 'swift/identity_map'
require 'swift/migrations'
class User < Swift::Scheme
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: '[email protected]' # => User
User.first(':name = ?', 'James Arthurton')
User.first(':name = ?', 'James Arthurton') # Gets same object reference
Bulk inserts
Swift comes with adapter level support for bulk inserts for MySQL, PostgreSQL and DB2. 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'
Swift.setup :default, Swift::DB::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.
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 github.com/shanna/swift/tree/master/benchmarks
Benchmarks
ORM
The following bechmarks were run on a machine with 4G ram, 5200rpm sata drive, Intel Core2Duo P8700 2.53GHz and stock PostgreSQL 8.4.1.
-
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 0.800000 6.620000 7.420000 9.898821 369.44m ar #select 0.020000 0.300000 0.320000 0.372809 38.83m ar #update 0.770000 6.550000 7.320000 10.02434 361.92m dm #create 0.110000 3.590000 3.700000 4.847609 248.74m dm #select 0.120000 1.840000 1.960000 2.029552 128.98m dm #update 0.400000 7.750000 8.150000 9.741249 599.52m sequel #create 0.770000 3.910000 4.680000 7.432611 263.59m sequel #select 0.020000 0.080000 0.100000 0.147321 9.82m sequel #update 0.730000 3.910000 4.640000 7.594949 259.18m swift #create 0.210000 0.850000 1.060000 2.618661 32.72m swift #select 0.000000 0.080000 0.080000 0.132245 9.91m swift #update 0.270000 0.650000 0.920000 2.204108 37.48m -- bulk insert api -- swift #write 0.000000 0.080000 0.080000 0.151146 7.29m
Adapter
The adapter level SELECT benchmarks without using ORM.
-
Same dataset as above.
-
All rows are selected 5 times.
-
The pg benchmark uses pg_typecast gem to provide typecasting support for pg gem and also makes the benchmarks more fair.
PostgreSQL
benchmark sys user total real rss
do #select 0.060000 1.070000 1.130000 1.370092 99.83m
pg #select 0.030000 0.270000 0.300000 0.584091 46.13m
swift #select 0.020000 0.290000 0.310000 0.571635 39.08m
MySQL
benchmark sys user total real rss
do #select 0.030000 1.070000 1.100000 1.200177 99.26m
mysql2 #select 0.060000 0.450000 0.510000 0.609236 76.44m
swift #select 0.050000 0.170000 0.220000 0.334932 33.61m
TODO
-
More tests.
-
Make db2 async api more stable.
-
Assertions for dumb stuff.
-
Abstract interface for other adapters? Move dbic++ to Swift::DBI::(Adapter, Pool, Result, Statment etc.)
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.