論盡蛋
心で書く

Blog


ActiveRecord::RecordNotUnique and Postgresql

OK I am not sure if this is a postgresql-specific problem or not, but I believe it is. If you did something like this (such as in seed.rb):

#!ruby
User.create(id: 1, ...)

Then the next time you create new user, the ActiveRecord::RecordNotUnique error would be raised.

But why you specified id!?

I have to admit that this is a lazy and silly decide. I re-created ChordsPresent and migrated from Heroku bamboo stack to cedar stack. The models have some schema-level changes also. Thus the easier way for me to migrate the old data to the new database is:

  1. download the remote database
  2. create a rake file to output a seed.rb file which based on the existing data (collecting only the required data, and matching the new schema)
  3. add the outputted seed.rb file to the new app source code, and push and run it in the cedar stack (so the new database now has all the new data with correct schema)
  4. revert the seed.rb and destroy the last commit completely (git tracking sensitive data is always a bad practice!, although the most sensitive data are the users' provider and uid only)
  5. push to heroku again with -f flag to force replace it

As I want to retain the chords and users id, thus I included the id field of both table. However explicitly assigning the id field causes problem.

How to fix?

(Provided this is a postgresql problem)

#!ruby
ActiveRecord::Base.connection.execute("SELECT setval('table_name_id_seq', (SELECT max(id) FROM table_name));")

replace the table_name with your actual table name. In my case, it is:

#!ruby
ActiveRecord::Base.connection.execute("SELECT setval('users_id_seq', (SELECT max(id) FROM users));")
ActiveRecord::Base.connection.execute("SELECT setval('chords_id_seq', (SELECT max(id) FROM chords));")

Each validator

Starting from rails 3.0, there is a class ActiveModel::EachValidator located at activemodel/lib/active_model/validator.rb.

In a word, the validates validation callback you use in an active record model is simply a call to a method of an each_validator.

In this article, I am going to show you the use of EachValidator through making a validtion on checking if there is only one occurance of a given value in the database (i.e. you could have only one row of record having the value of admin in the role column on the user model).

code

Model

#!ruby app/models/user.rb
class User < ActiveRecord::Base
  # column: (string) role

  # you want the following works:
  validates :role, have_only_one: { value: :admin }
end

The above code would show you an exception that there is no HaveOnlyOneValidator class. So let's create one.

Validator

#!ruby
class HaveOnlyOneValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    restricted_value = options[:value]

    if value == restricted_value && record.class.where(attribute => restricted_value).where("`#{record.class.table_name}`.`id` != #{record.id}").count > 0
      record.errors[attribute] << I18n.t(:"#{record.class.i18n_scope}.errors.models.#{record.class.name.underscore}.attributes.#{attribute}.#{type}")
    end
  end
end

There are two conditions in line 5:

  1. if you set the value to :admin (i.e. the restricted value)
  2. if there are more than 0 other records that already has :admin value

then we add an error. If there is an error being added, the validation would fail.

You may found that there is a local variable options. It would be the hash you pass with have_only_one: {value: :admin}.

And finally, if the validator class name is SomeValidator < ActiveModel::EachValidator, you should use it by: validates :attr_name, some: true # or { if: ... }

Validator Location

Finally, where should I store the validator class? You could have two choices.

The first choice is to place it above the model which uses it. i.e. just above the User model in app/models/user.rb.
This is good if the validator

  • is model-specific
  • will only be used by the model in the visible future

If the validator would be used in two more more models, you should consider putting it in the /lib directory, which is the second choice.

I would suggest creating a /lib/validators directory and put all your custom validators there. By doing so, you know what validators you have defined, you could easily copy the validators to other projects if you want......


Rails 3.2 .railsrc

幾乎每次開新的 Rails project,我都會用這些參數:rails new project-name -T --skip-bundle

-T 是因為要用 RSpec 和 Cucumber。--skip-bundle 是因為我將會用 rvm 來建立一個 gemset 給這個 project,bundle 完在新的 gemset 又要再 bundle 一次,只會浪費時間。

但自從 Rails 3.2 開始,你可以建立一個 .railsrc 檔案,在裡面指定的參數就會自動被應用。

以下就是我的 .railsrc:

#!bash
-T --skip-bundle

看可以用的參數:rails --help

你也可能會想加入 -d mysql (但我很喜歡 SQLite XD")


Bi-directional hash

Recently found this:

#!ruby
hash = { 0 => :a, 1 => :b, a: 0, b: 1}
h[0] # :a
h[:a] # 0

Note on the syntax, 0 => :a CANNOT be replaced with 0: :a, which would raise syntax error.

Usage

In my rails development, there are chances that I want to store the value into the database, but in the source code I do not want to directly write the integer...

#!ruby
class User < ActiveRecord::Base
  STATUSES = { 0 => :active, 1 => :inactive, :active => 0, :inactive => 1 }

  def status
    STATUSES[read_attribute(:status)].to_sym
  end
  def status=(status_str)
    write_attribute(:status, STATUSES[status_str.to_sym])
  end
end

User.new(:status => :active) # this is much better than :status => 0, after 1 months you don't know what is 0!

There is one more improvement though. It is too redundant to write STATUSES = { 0 => :active, 1 => :inactive, :active => 0, :inactive => 1 }.

#!ruby
STATUSES = %w{ active inactive }.each_with_object({}) do |status, statuses|
  i = statuses.count / 2
  status = status.to_sym
  statuses[i] = status
  statuses[status] = i
end

As values inside %w{} are strings, we convert it to symbol during the block. If you prefer string over symbol, just remove the conversion line.


Setup Cucumber

This post will show how to add cucumber (http://cukes.info) support for acceptance testing. This is in addition to the previous post Setup the Test Environment.

We are going to install cucumber, as well as adding support for spork and guard in order to load cucumber features faster.

Finally, don't forget the simplecov. It supports both rspec and cucumber :)

Code

1) Install cucumber

add this to the Gemfile and the run bundle:

#!ruby
group :test do
  gem 'cucumber-rails'
  # database_cleaner is not required, but highly recommended
  gem 'database_cleaner'
end

after the bundle command finished, run the generator:

#!bash
rails generate cucumber:install

Now, you may run cucumber by running this command:

#!bash
cucumber features
# or
rake cucumber

2) Spork support for cucumber

Still remember how did we add spork support for rspec? We ran spork --bootstrap. However, if we run the same command again, it should only work for rspec. For cucumber, we do the following:

#!bash
spork cucumber --bootstrap

To start a spork for rspec, we run spork. For cucumber, we need:

#!bash
spork cucumber

Now, before we actually run the spork command, let's update the features/support/env.rb so that spork loads correctly. The instructions of how to update the env.rb have been added to the env.rb file when you run spork cucumber --bootstrap.

To use spork with cucumber, run these in two consoles:

#!bash
spork cucumber

#!bash
cucumber --drb features

3) Guard cucumber

add this to Gemfile and run bundle

#!ruby
group :test do
  gem 'guard-cucumber'
end

after that, run:

#!bash
guard init cucumber

The update have been added to the Guardfile. You may have a look to see if you need any customization. But for me it's OK already :)

Now guard command will run both cucumber and rspec in the same time.

4) simplecov

add this single line to the top of the features/support/env.rb:

#!ruby
require 'simplecov'
# ... rest of the file

To generate coverage report, same as the rspec, just directly run cucumber features instead of running cucumber from guard.

Now, all are done :)