Saturday, May 31, 2008

Give me :five

Yes, give me :five! Enough :first and :all, I want to find :five.

In order to do this, I checked the source code of find :first and find :all in base.rb

base.rb

def find(*args)
options = args.extract_options!
validate_find_options(options)
set_readonly_option!(options)

case args.first
when :first then find_initial(options)
when :all then find_every(options)
else find_from_ids(args, options)
end
end


OK, the next step is easy, override this method by defining a class method of a Model class, let's say, Blog class and add :five argument in.

Use class << self to open the Blog class object and add the find method in to override the original one. You have to implement :first and :all again. Otherwise the "Blog.find :all" will not work.

blogs.rb

class Blog < ActiveRecord::Base
has_many :posts

class << self
def find(*args)
options = args.extract_options!
validate_find_options(options)
set_readonly_option!(options)

case args.first
when :first then find_initial(options)
when :all then find_every(options)
when :five then find :first,
:limit => 1,
:offset => 4 #call :first instead
else find_from_ids(args, options)
end
end
end
end


Now, you can use find :five to retrive the fifth elements:


>>b5=Blog.find :five


Bingo, I got a :five!

Friday, May 30, 2008

The one I love most

If a Model class has a "has_many" relationship available, it can specify a "has_one" relationship pointing to one record for future convenient reference.

:conditions and other SQL options can be used to narrow the search.

Blog has many Post


class Blog < ActiveRecord::BASE
has_many :posts #has_many must be present
has_one :first_post, #no naming convention.
:class_name => 'Post', #must specify!
:order => 'created_at asc'
end

class Post < ActiveRecord::BASE
belongs_to :blog #one belongs_to is enough
end


Later, you can reference the first record easily by:

>>blog1 = Blog.find :first
>>blog1.first_post


Yes! this is the one I love most, give her some special treat.

To be singular or to be plural

ActiveRecord's relationship is sensitive to the plurality of the database table name.

Let's say, blogs contains many posts, then blogs should be on singular side and posts on plural side.


class Blog < ActiveRecord::BASE
has_many :posts #a plural declare
end


class Post < ActiveRecord::BASE
belongs_to :blog #a singular declare
end


For has_one and belongs_to relationship, it should be singular. Suppose each user has only one avatar:


class Avator < ActiveRecord::BASE
belongs_to :user #a singular declare
end


class User < ActiveRecord::BASE
has_one :avatar #a singular declare
end


If you don't follow the convention, it won't work.

Wednesday, May 28, 2008

Use YAML to load sample data into database

When reading the Active Record, I was wondering if I can create a rake to automatically populate some data into database.

After some research, I have the following code samples.

Suppose I have three tables: blogs, posts and comments.

And I have three prepared yaml files: blogs.yml, posts.yml and comments.yml. They are saved at /#{RAILS_ROOT}/lib/tasks/sample_data/

blogs.yml looks like:

B1000:
id: 1000
blog_name: "Xianese Photo"

B2000:
id: 2000
blog_name: "Mark's World"

B3000:
id: 3000
blog_name: "May's Beautiful World"




Then the rake file "sample_data.rake" is saved under
/#{RAILS_ROOT}/lib/tasks/.

I use TABLES array to hold the table names.

TABLES = %w-blogs comments posts-


From table names, I use Inflector's classify function to convert the table names to Model Class names and then use const_get function to get the ActiveRecord Class instance. You can't directly use a string name to refer to the Class instance. So this step is necessary.


path = DATA_DIRECTORY + table_name.to_s + '.yml'
model_name = Inflector.classify table_name
model_class = ActiveRecord.const_get(model_name)


Next step is easy, ust YAML.load_file to load each yml files:


data = YAML.load_file(path)


Notice that the data is actually a hash like "B1000" => <...>. The <...> is another hash with real data: blog_name => "Xianese's blog", etc. So you have to tranverse the hash then use model_class to create a new instance of the Model object and save it:


data = YAML.load_file(path)
data.each do |key, value|
model_instance = model_class.create(value)
model_instance.save
end


That is it! It is pretty simple.

Removing sample data is even simpler. I use ruby's eval method to pass in a class string name and delete all records for each Model.

Here is the final code:


require 'yaml'

namespace :db do
DATA_DIRECTORY = "#{RAILS_ROOT}/lib/tasks/sample_data/"

namespace :sample_data do
TABLES = %w-blogs comments posts-

desc 'Load the sample data'
task :load => :environment do |t|
TABLES.each do |table_name|
path = DATA_DIRECTORY + table_name.to_s + '.yml'
model_name = Inflector.classify table_name
model_class = ActiveRecord.const_get(model_name)

data = YAML.load_file(path)
data.each do |key, value|
model_instance = model_class.create(value)
model_instance.save
end

puts "Loaded data from #{table_name}.yml"
end
end

desc 'Remove the sample data'
task :remove => :environment do |t|
TABLES.each do |table_name|
model_name = Inflector.classify table_name
eval "#{model_name}.delete_all"
end
end

end
end



I hope this helps.

Happy blogging to myself!

I am going to use this blog to build my rails. A rails to sucess!