Sunday, October 26, 2008

Connecting to Multiple Databases in Ruby on Rails

If you keep different tables in different databases, you've probably been looking for a way to allow Rails to connect to more than one database. You've probably found that you can add another configuration (besides development, test and production) to config/database.yml. There's a problem with that though. If you added a configuration called foo, you are now always connecting to foo, in production mode, test mode and development mode. This is especially bad in test mode, since the database would get wiped, but you probably want a different database for development and production too.

Here's how to do it. Suppose you have already set up your regular development/test/production databases in config/database.yml:

development:
  adapter: jdbc
  driver: com.mysql.jdbc.Driver
  url: jdbc:mysql://devdb.yourdomain.com:3306/mstore
  username: cart
  password: secret

# Warning: The database defined as 'test' will be erased and
# re-generated from your development database when you run 'rake'.
# Do not set this db to the same as development or production.
test:
  adapter: jdbc
  driver: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/mstore
  username: cart
  password: secret

production:
  adapter: jdbc
  driver: com.mysql.jdbc.Driver
  url: jdbc:mysql://proddb.yourdomain.com:3306/mstore
  username: cart
  password: secret

Now you decided to store all the user account information in a different database. You still want a separate development, test and production databases for your account information. You'll need to add three more entries to the database configuration then, one for each of the account databases. You'll need to name them with an appropriate suffix to specify development/test/production:

account_development:
  adapter: jdbc
  driver: com.mysql.jdbc.Driver
  url: jdbc:mysql://devdb.yourdomain.com:3306/account
  username: cart
  password: secret

# Warning: The database defined as 'test' will be erased and
# re-generated from your development database when you run 'rake'.
# Do not set this db to the same as development or production.
account_test:
  adapter: jdbc
  driver: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/account
  username: cart
  password: secret

account_production:
  adapter: jdbc
  driver: com.mysql.jdbc.Driver
  url: jdbc:mysql://accountdb.yourdomain.com:3306/account
  username: cart
  password: secret

Note that these account databases could be on the same hosts or on different hosts from the main databases. Now on to the models. You need to create a base class model for all your account tables, so that only one connection to the account database gets created. If you just call establish_connection in every model, it'll open that many database connections, one for each model. Here's a base model for the account tables:

class AccountBase < ActiveRecord::Base
  # No corresponding table in the DB.
  self.abstract_class = true

  # Open a connection to the appropriate database depending
  # on what RAILS_ENV is set to.
  establish_connection("account_#{RAILS_ENV}")
end

Now your account models need to inherit from AccountBase instead of ActiveRecord::Base, like this:

class Login < AccountBase
end

Because it inherits from AccountBase, it'll use the database connection already established by AccountBase.

Monday, October 20, 2008

/etc/hosts and Mac OS X 10.5 Leopard

Instead of using /etc/hosts on Mac OS Leopard, the proper way to add hosts is using the dscl utility. First, let's list the hosts already there:
$ dscl localhost -list /Local/Default/Hosts
$
Nothing. Let's add one. I want to be able to access localhost as me.blogger.com. Right now that doesn't work:
$ ping -q -c 1 me.blogger.com
ping: cannot resolve me.blogger.com: Unknown host
$
Create an entry for it and try again:
$ sudo dscl localhost -create /Local/Default/Hosts/me.blogger.com \
  IPAddress 127.0.0.1
$ ping -q -c 1 me.blogger.com
PING me.blogger.com (127.0.0.1): 56 data bytes

--- me.blogger.com ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.077/0.077/0.077/0.000 ms
You can list the hosts you have set up, where they point, and you can delete them:
$ dscl localhost -list /Local/Default/Hosts
me.blogger.com
$
$ dscl localhost -read /Local/Default/Hosts/me.blogger.com
AppleMetaNodeLocation: /Local/Default
IPAddress: 127.0.0.1
RecordName: me.blogger.com
RecordType: dsRecTypeStandard:Hosts
$
$ sudo dscl localhost -delete /Local/Default/Hosts/me.blogger.com
$ dscl localhost -list /Local/Default/Hosts
$

Wednesday, August 13, 2008

CyberSource, SOAP and Ruby

CyberSource has a SOAP API. They don't officially support Ruby, but I don't see why it shouldn't work. Ruby has built-in support for SOAP. I knew absolutely nothing about SOAP or WSDL an hour ago, so I'm still trying to figure out how this works. SOAP is a protocol for calling web services and getting the responses from them. WSDL files describe the available web services. It looks like CyberSource publishes a new WSDL file for every API version that it supports. The latest at the moment seems to be 1.38. You can see a list of their WSDL files here. Let's try something:

require "soap/wsdlDriver"

wsdl_file = "https://ics2ws.ic3.com/commerce/1.x/" +
  "transactionProcessor/CyberSourceTransaction_1.38.wsdl"

# Create a driver from the definition provided in wsdl_file.
driver = SOAP::WSDLDriverFactory.new(wsdl_file).create_rpc_driver

# See what's available in this driver.
puts driver.methods.sort.join("\n")

This prints out a long list of method names available on the driver that is created from the WSDL file. AFAICT, the only method you're supposed to call is runTransaction. I don't know how to call it yet.

Sunday, August 10, 2008

MySQL On Mac OS X Using MacPorts

If you only want to use the MySQL client, all you have to do is install it using the port command:

sudo port install mysql5

You can now connect to remote servers with something like this:

mysql5 -usomeuser -psomepassword -hsomehost.somedomain.com

However, if you want to run a MySQL server on your local machine, there are few more things that you have to do. First, you need to initialize MySQL system tables:

sudo mysql_install_db5 --user=mysql

This initializes MySQL databases under /opt/local/var/db/mysql5/ . We also need to create a directory under MacPorts that is identical to /var/run:

$ ls -ld /var/run
drwxrwxr-x  27 root  daemon  918 Aug 10 09:07 /var/run
$ sudo mkdir /opt/local/var/run
$ sudo chgrp daemon /opt/local/var/run
$ sudo chmod g+w /opt/local/var/run
$ ls -ld /opt/local/var/run
drwxrwxr-x  2 root  daemon  68 Aug 10 15:31 /opt/local/var/run

You can now start the MySQL server, but it is NOT SECURE yet. Make it only listen for local connections using the --bind-address option. This makes it impossible to connect to MySQL from the outside. If you intend this MySQL installation to be used for development only, you should always start it like this. If you want to use this in production and connect to it from other machines, you'll need to remove the --bind-address option, but only AFTER SECURING THE INSTALLATION.

sudo mysqld_safe5 --bind-address 127.0.0.1 > /dev/null 2>&1 &

MySQL comes with a nice script to secure it, called mysql_secure_installation. Unfortunately, if you installed MySQL using the sudo port install mysql5 command, the script will fail, because it's looking for the mysql command to be in your path, but the command is really called mysql5. You can make a symlink to fix it, though I'm not sure if this will clash with future MacPorts MySQL installations. You can remove the symlink after you're done if you wish.

sudo ln -s mysql5 /opt/local/bin/mysql

Now you can run the secure script. Be sure to set the root password, remove anonymous users, disable remote root login, remove the test database, and reload privilege tables. Basically answer Y to everything.

sudo mysql_secure_installation5

You should now be able to connect using your new root password (you'll be prompted for it):

mysql5 -uroot -p

To remove the symlink you made:

# Make sure it's really a symlink to mysql5.
$ ls -l /opt/local/bin/mysql
lrwxr-xr-x  1 root  admin  6 Aug 10 16:11 /opt/local/bin/mysql -> mysql5
$ sudo rm /opt/local/bin/mysql

To shut down MySQL, you can use the mysqladmin command. It will prompt for your MySQL root password:

mysqladmin5 shutdown -uroot -p

If you are planning to use this server in production, you should probably create a my.cnf with all the needed options, and make MySQL start up when the machine boots. You should also create some non-root users, even if it's only used for development. But this is a topic for another time.

Wednesday, August 6, 2008

Ruby On Rails on Mac OS X

My laptop already had ruby installed (part of the standard OS install), but I decided to get the latest versions and recompile them. Thankfully this is trivial with MacPorts. To install MacPorts, you'll need to have the developer's toolkit installed (gcc, make, etc) - this on the second CD that comes with the laptop.

Once you have MacPorts installed, make sure it's in your path, put this in your .bash_profile :

# MacPorts
PATH=/opt/local/bin:/opt/local/sbin:$PATH; export PATH
MANPATH=/opt/local/share/man:$MANPATH; export MANPATH

Now install ruby and rubygems:

sudo port install ruby rb-rubygems

Use gem to install rails:

sudo gem install rails

Mongrel is much better than WEBrick, so install it:

sudo gem install mongrel
You'll need a database to start using Rails. I'll probably install a local MySQL for that.

Tuesday, July 29, 2008

Converting from Subversion to Mercurial

Nice post about converting your Subversion (svn) code repository to Mercurial (hg):

SNH

Thursday, June 5, 2008

Timezone conversion in Ruby

The time in the feed is supplied with 2 fields - date and time. It is in UTC. The date looks like 20080605, and the time looks like 142115. I need to display the current local date and time. I could've used a regular expression to parse the date/time into components, but I wanted to use strptime. The Time module does not have strptime, but the DateTime module does. Once parsed, the DateTime object is in UTC. To get local time, use the new_offset method.
require "Date"

# time_str will look like "20080605 142115 UTC".
time_str = feed_date + " " + feed_time + " UTC"

# Parse time_str into a DateTime object.
dt = DateTime.strptime(time_str, "%Y%m%d %H%M%S %Z")

# local_dt will contain the date and time in the local timezone.
local_dt = dt.new_offset(DateTime.now.offset)

display_dt_str = local_dt.strftime("%Y-%m-%d %H:%M")

Friday, May 23, 2008

Dynamically adding methods to objects

Let's say you have a class named Color, which defines instance methods red, green and blue, which return the corresponding color component (0 to 255). You have an instance of Color called santorin, among other instances. You just calculated a transparency component for this color and want to add that in. Problem is, the Color class doesn't have a transparency attribute. We don't want transparency in any other instance of this class anyway. The proper way to do this would probably be to subclass Color as TransparentColor and add a transparency attribute there. But that's more code than we want for this simple task. You could just define an extra method for the santorin instance:
def santorin.transparency
  5
end
This works, and will return 5 when you call it. But our calculated transparency is in a variable called trans, so we need to do this:
def santorin.transparency
  trans
end
Now that doesn't work, because when you try to call this method, you'll get this:
NameError: undefined local variable or method `trans' for ...
  ...
The problem is that trans is evaluated when you call the method, not when you define it. The quickest solution here is to use eval to force evaluation of the variable at definition time:
eval "def santorin.transparency() #{trans}; end"