Articles Feed

Authors

Categories

Sharing a Database Among Multiple Rails Applications

by: jim | February 20th, 2007 | 4 comments »

A sign of the success of Ruby on Rails is the size of applications being written with it. Currently, we are working on a project with many interconnecting modules that we split up into separate Rails applications. However, the business domain was the same among the projects, resulting in the different projects sharing the same database. This article discusses the way we’ve accomplished this.

Create a Lib Project
We first need a place to pull our common classes into. This will not be a Rails application; it will just be a Ruby module that your Rails application will require. Our directory structure will look something like this:

/projects/my_lib --> lib --> spec

Now we can move on to the actual refactoring.

Pull the Models Out
The first step is the easiest: pull out the models from the app/models directory of your Rails application into the lib directory of your new library. In the config/environments.rb file within your Rails application, you will need to add this new directory to your Ruby search path by adding this line to the end of the file.

  1. $: << File.dirname(__FILE__) + '/your_lib_path'

More on the Ruby search path later.

Pull the Tests Out
Let’s move on to making our specs pass. Now that we don’t have the Ruby environments loaded up as part of our test helpers (spec_helper for the rspecers out there), we need to establish an Active Record connection ourselves. Create a spec helper for our lib specs and start it with the connection to the database like so.

  1. require 'rubygems'
  2. require 'active_record'
  3. ActiveRecord::Base.establish_connection(
  4. :adapter => "postgresql",
  5. :host => "localhost",
  6. :username => "rails",
  7. :password => "",
  8. :database => "<db name>_test"
  9. )

This is the same way Rails sets up your database connections for active record when you load the Rails environment. Now your specs just need to require this new spec_helper in order to get Active Record functioning.

Use Models Across Projects
Now you are free to use the same database across many projects. To do this, you just need to either require a single lib file that loads all the models, or you could put the lib directory in your Ruby search path.

Ruby Search Path vs. Lib Require File
The new common library will need to be included in your rails application. There are two methods we have used to do this. The first is to create a library file that does nothing more than requires all of the common lib files, looking something like the following.

  1. require File.expand_path(File.dirname(__FILE__) + '/my_lib/lib/file_a'
  2. require File.expand_path(File.dirname(__FILE__) + '/my_lib/lib/file_b'
  3. require File.expand_path(File.dirname(__FILE__) + '/my_lib/lib/file_c'
  4. require File.expand_path(File.dirname(__FILE__) + '/my_lib/lib/file_d'
  5. require File.expand_path(File.dirname(__FILE__) + '/my_lib/lib/file_e'

The problem with this approach is it requires you to sequence the require statements in the appropriate order, or else you will get errors about missing objects. The benefit is that you load the entire environment in one place rather than adding require statements at the top of each file to include the needed external dependencies. These code files that do nothing but require files are no stranger to Ruby; however, they must be managed properly as they become temperamental and irritable when dependencies are mixed up.

The other approach we have used is what I recommended above. In your Rails application’s environment, add the lib directory to your search path, and include the require statements as needed in each model file. Of course, by including one file you will be implicitly including all of the dependencies. Using this technique, you can eliminate files that just require other files.

Storing Binary Data in Postgres? Beware!

by: micah | February 18th, 2007 | 0 comments »

If your situation matches the following conditions, beware!

  1. You’re working in rails.
  2. You’re using Postgresql.
  3. You’re storing binary data in the database.

This was the situation on a project of mine. We were storing PDFs and PNG images in our Postgrs database. Everything was fine during development and testing where we used files that ranged up to a few dozen kilobytes in size. The situation went rapidly downhill when file sizes got up in the hundreds of kilobytes to megabytes. The worst part about it was that the errors we got were misleading and seemingly random. They included:

These errors sent us on a wile goose chase. The clue that finally pointed us toward the problem was the fact that it took 6 seconds to load a 4M PDF document from the database. That was far too long especially considering the same document could be loaded from a file instantaneously.

Apparently, binary data stored in Postgresql’s bytea data type has to be parsed on save and load to escape and unescape certain characters. Unfortunately the native C postgres gem doesn’t do the parsing. It’s done in the PostgreSQLAdapter.unescape_bytea and PostgreSQLAdapter.escape_bytea methods (Ruby code) of ActiveRecord and the parsing is a bit too intensive for Ruby. This is where the meltdown begins. It consumes too much memory, or too much processing power, or … well I don’t know exactly. But I know it breaks.

We refactored our model such that all the binary data gets stored in flat files on disk rather than in the database. After this, our Rails app came back to life. It was MUCH faster too!

Here’s hoping that if and when you encounter this problem, Google points you to this blog entry and you find it helpful.

Understanding Statemachines, Part 3: Conditional Logic

by: micah | February 12th, 2007 | 0 comments »

Conditions

If you’re doing any significant amount of work with statmachines, you will most certainly encounter some conditional logic in your statemachines. Take our vending machine. When ever a coin is inserted, the invoked event will depend on whether the total amount of money inserted is sufficient to buy something. If enough money has been tendered, the display should suggest that the customer make a selection. If insufficient money has been inserted, the customer should be prompted to insert more.

Conditional logic can be accomplished by using entry actions. See the diagram below.


State Diagram with Conditional Logic

Starting in the Accept Money state, when a coin is inserted, the coin event is fired and the statemachine transitions into the Coin Inserted state. This is where it gets fun. Upon entering of the Coin Inserted state its entry event is invoked: count_amount_tendered. This method will count the money and invoke the not_paid_yet or paid event accordingly. This will cause the statemachine to transition into the appropriate state.

The Coin Inserted state is unique. You wouldn’t expect to find the statemachine in the Coin Inserted state for any reason except to make this decision. Once the decision is made, the state changes. States like this are called Decision States.

Code

  1. require 'rubygems'
  2. require 'statemachine'
  3. class VendingMachineContext
  4. attr_accessor :statemachine
  5. def initialize
  6. @amount_tendered = 0
  7. end
  8. def add_coin
  9. @amount_tendered = @amount_tendered + 25
  10. end
  11. def count_amount_tendered
  12. if @amount_tendered >= 100
  13. @statemachine.paid
  14. else
  15. @statemachine.not_paid_yet
  16. end
  17. end
  18. def prompt_money
  19. puts "$.#{@amount_tendered}: more money please"
  20. end
  21. def prompt_selection
  22. puts "please make a selection"
  23. end
  24. end
  25. vending_machine = Statemachine.build do
  26. trans :accept_money, :coin, :coin_inserted, :add_coin
  27. state :coin_inserted do
  28. event :not_paid_yet, :accept_money, :prompt_money
  29. event :paid, :await_selection, :prompt_selection
  30. on_entry :count_amount_tendered
  31. end
  32. context VendingMachineContext.new
  33. end
  34. vending_machine.context.statemachine = vending_machine
  35. vending_machine.coin
  36. vending_machine.coin
  37. vending_machine.coin
  38. vending_machine.coin

Output:

$.25: more money please $.50: more money please $.75: more money please please make a selection

Next lesson: Superstates
Understanding Statemachines, Part 4: Superstates