active_record snippets

How to update the ActiveRecord counter_cache magic column

Tagged update, counter_cache, sql, rails, active_record  Languages sql

You can use the model.update_counters method to update the counter_cache column. But if you have a million rows it be very fast.

So for large tables it's best to do it with a query such as this:

update categories, (select 
                      id as category_id, ifnull(count, 0) as count
                    from categories left join 
                      (select category_id, count(id) as count from products group by category_id) as count_table 
                    on 
                      categories.id = count_table.category_id) as count_table
set 
  categories.products_count = count_table.count
where
  categories.id = count_table.category_id

This query updates the count for all rows.

The code needs to be modified for your database design.

Twitter type "followers, following database schema" for Rails/ActiveRecord

Tagged twitter, follower, following, schema, active_record  Languages ruby

Draft schema for Twitter type followers, following functionality:

create_table "followers", :force => true do |t|
    t.integer "leader_id"
    t.integer "follower_id"
end

add_index "followers", ["leader_id", "follower_id"], :name => "index_followers_on_leader_id_and_follower_id", :unique => true

Just an idea, haven't tested this yet.

Update: railscasts.com has published an article on how to use self-referential associations for this purpose: http://railscasts.com/episodes/163-self-referential-association

Validating URLs in Rails

Tagged rails, validation, active_record, url  Languages ruby

Minimalist implementation

A naïve implementation:

validates_format_of :website, :with => URI::regexp(%w(http https))

via http://mbleigh.com/2009/02/18/quick-tip-rails-url-validation.html

Full-featured and reusable implementation

A more full-featured and reusable example using the Addressable gem (app/validators/uri_validator.rb):

require 'addressable/uri'

#
# Validates URLs.
#
# @param options[:allow_uri] If true, validates only URI part of URL and skips host and scheme.
# @param options[:message] Error message.
# @param options[:allowed_protocols] Allowed protocols, e.g. [:http, :https].
#
class UriValidator < ActiveModel::EachValidator

  def validate_each(record, attribute, value)
    uri = parse_uri(value)
    if !uri
      record.errors[attribute] << generic_failure_message
    elsif !allowed_protocols.include?(uri.scheme)
      record.errors[attribute] << "must begin with #{allowed_protocols_humanized}"
    end
  end

private

  def generic_failure_message
    options[:message] || "is an invalid URL"
  end

  def allowed_protocols_humanized
    allowed_protocols.to_sentence(:two_words_connector => ' or ')
  end

  def allowed_protocols
    @allowed_protocols ||= options.fetch(:allowed_protocols, ['http', 'https'])
  end

  def parse_uri(value)
    uri = Addressable::URI.parse(value)
    uri.scheme && uri.host && uri.to_s == value && uri
  rescue URI::InvalidURIError, Addressable::URI::InvalidURIError, TypeError
  end
end

Tests:

require 'spec_helper'

describe UriValidator do
  let(:subject) do
    Class.new do
      include ActiveModel::Validations
      attr_accessor :url
      validates :url, uri: true
    end.new
  end

  VALID_URL = [ 'http://www.google.com',
                'http://.com', # NOTE: Addressable thinks this is valid
                'http://google' ]
  VALID_URL.each do |valid_url|
    it "accepts a valid URL (#{valid_url})" do
      subject.url = valid_url
      assert subject.valid?
      subject.errors.full_messages.should == []
    end
  end

  INVALID_URL = ['http://ftp://ftp.google.com',
                 'http://ssh://google.com',
                 '//www.google.com',
                 'www.google.com',
                 'google.com',
                 'http:/www.google.com',
                 '<>hi' ]
  INVALID_URL.each do |invalid_url|
    it "#{invalid_url} is an invalid url" do
      subject.url = invalid_url
      subject.valid?
      binding.pry if subject.valid?
      subject.errors.should have_key(:url)
      subject.errors[:url].should include("is an invalid URL")
    end
  end

  INVALID_PROTOCOL = [ 'ftp://ftp.google.com',
                       'ssh://google.com' ]
  INVALID_PROTOCOL.each do |invalid_url|
    it "#{invalid_url} is an invalid url" do
      subject.url = invalid_url
      subject.valid?
      subject.errors.should have_key(:url)
      subject.errors[:url].should include("must begin with http or https")
    end
  end
end

Code based on http://gist.github.com/bf4/5320847.

I18n for ActiveRecord Model Attributes

Tagged active_record, i18n  Languages ruby

This code will translate the AR model's title attribute using Rails' I18n library:

class Link < ActiveRecord::Base
  I18N_ATTRIBUTES = [ :title ]
  I18N_ATTRIBUTES.each do |attr|
    class_eval <<-EORUBY, __FILE__, __LINE__ + 1
      def #{attr}
        I18n.t(self[:#{attr}], default: self[:#{attr}])
      end
    EORUBY
  end
end

If a translation is not defined, the code will fall back to use the attribute's original value.

Example with translation defined:

Translation file (config/locales/en.yml):

en:
  views:
    index:
      title: Hello

Model code (app/models/link.rb):

link.title = 'views.index.title'
# Uses string from config/locales/en.yml
link.title => "Hello"

Example without translation defined

link = Link.new title: 'Hello'
# Fall back to specified value, because no translation is defined
link.title => "Hello"