I'm in the process of trying to duplicate a record and all of its subsequent child associations which are deeply nested. Below is a simplification of the problem.
I have the following Foo model:
class Foo < ActiveRecord::Base
belongs_to :project,
inverse_of::foos
validates :name,presence:true,uniqueness:{ scope: :project_id },
format:{ with: /[0-9a-zA-Z\s\/_:\-.|]*/ }
end
I'm duplicating the project like this:
origin = Project.first
clone = origin.dup
clone.foos << clone_records(origin.foos)
clone.save
def clone_records(records)
clones = []
records.each do |record|
cloned_record = record.dup
cloned_record.project_id = nil
cloned_record.name = "Copy of " + record.name
# Which Generates:
#<Foo:0x007f94353fc200> {
# :id => nil,
# :name => "Copy of Some Foo",
# :project_id => nil
#
# }
clones.push(cloned_record)
end
return clones
end
The problem is, when I duplicate the project and assign the newly generated, renamed, foos, upon saving I get the error:
Foo Exists (0.4ms) SELECT 1 AS one FROM "foos" WHERE ("foos"."name" = 'Copy of Some Foo' AND "foos"."project_id" = 1) LIMIT 1
However, no foo with that name exists:
Foo.where(name: "Copy of Some Foo")
# Foo Load (0.6ms) SELECT "foos".* FROM "foos" WHERE "foos"."name" = $1 ORDER BY "foos"."id" ASC [["name", "Copy of Some Foo"]]
# []
Can anyone tell me what might be going on here? I gather that it has something to do with the validation, but I don't understand why: 1) It thinks this new record exists, and 2) why the new record has a project_id set to the origin project when I've explicitly nullified that field.
The problem is that uniqueness: { scope: :project_id } also applies to nil values. So if you have two records and they both have project_id == nil that would violate the uniqueness constraint.
Here is one way to allow nil values while still keeping the uniqueness constraint:
uniqueness: { scope: :project_id }, unless: Proc.new { |record| record.project_id.blank? }
When ActiveRecord complains that Foo Exists, it then rolls back the commit, so your cloned Foos do not persist to the database.
First, this line:
Foo Exists (0.4ms) SELECT 1 AS one FROM "foos" WHERE ("foos"."name" = 'Copy of Some Foo' AND "foos"."project_id" = 1) LIMIT 1
is not a error at all, it's just a log indicated Rails is running your uniqueness validation. And,
2) why the new record has a project_id set to the origin project when I've explicitly nullified that field.
Are you sure? it's project_id did set to the origin project? It's seems to me, the project_id is just set to the new created project's id.
Related
The DeployedSkill object references a ValuesList object which provides a limited value domain to users answers. An additional "filter" field contains an expression to filter even more the values of the domain. Values are instances of the Value class, belonging to the ValuesList object.
The model looks like this:
class DeployedSkill < Skill
### before filter
before_validation :set_code
# Custom validation of the filter attribute
validate :values_filter
validates :code, presence: true, uniqueness: {scope: :business_object_id, case_sensitive: false}, length: { maximum: 32 }
belongs_to :template_skill, :class_name => "DefinedSkill", :foreign_key => "template_skill_id" # helps retrieving the skill template
belongs_to :parent, :class_name => "DeployedObject", :foreign_key => "business_object_id"
belongs_to :values_list, inverse_of: :deployed_skills
### private functions definitions
private
### format code
def set_code
if self.code[0, 3] == "DV_"
self.code = self.code[3 .. -1]
end
self.code = "#{code.gsub(/[^0-9A-Za-z]/, '_')}".upcase[0,32]
end
def values_filter
begin
puts "----- Test the filter -----"
Value.where("#{filter}").first
rescue
puts "----- Filter test failed -----"
self.errors.add(:filter, "is not correctly specified")
puts self.errors.map {|error, message| [error, message]}.join(', ')
end
end
end
Before saving the updated DeployedSkill, I validate that the filter expression is compatible with a database query.
Using ByeBug, I can see that the test works fine, and an error is added to ActiveModel::Errors list when filter's syntax is not correct:
#<ActiveModel::Errors:0x0000000012bf03a8 #base=#<DeployedSkill id: 638, playground_id: 0,
business_object_id: 632, code: "SAKO1_METHODE", blablabla ... ,
type: "DeployedSkill", filter: "'xxx'", global_identifier: nil, sort_code: "SAKO1_METHODE",
physical_name: nil>,
#messages={:filter=>["is not correctly specified"]},
#details={:filter=>[{:error=>"is not correctly specified"}]}>
The console echoes it:
----- Test the filter -----
Value Load (10.2ms) SELECT "values".* FROM "values" WHERE (code like 'S%) ORDER BY "values"."id" ASC LIMIT $1 [["LIMIT", 1]]
----- Filter test failed -----
filter, is not correctly specified
(0.8ms) ROLLBACK
Nevertheless, the update method instruction:
if #deployed_skill.update_attributes(deployed_skill_params)
breaks into an error:
PG::InFailedSqlTransaction: ERROR: current transaction was aborted, commands are ignored until the end of the transaction :
SELECT exists(
SELECT * FROM pg_proc
WHERE proname = 'lower' AND proargtypes = ARRAY['character varying(255)'::regtype]::oidvector )
OR exists( SELECT * FROM pg_proc INNER JOIN pg_cast ON ARRAY[casttarget]::oidvector = proargtypes
WHERE proname = 'lower' AND castsource = 'character varying(255)'::regtype )
At this point, I have no idea of what's going on, and I don't know where to search deeper.
I hope you can help me to understand the issue and find the solution!
Thanks!
I would like to return values in json that contain models to be associated using JBuilder.
But I don’t know how to do it. And I encounter the error that “undefined method xx”
Here is my setting of rails.
Model
app/models/item.rb
belongs_to :user
app/models/user.rb
include UserImageUploader[:image]
has_many :item
vim app/uploaders/user_image_uploader.rb
# MiniMagick
require 'image_processing/mini_magick'
class UserImageUploader < Shrine
include ImageProcessing::MiniMagick
# The determine_mime_type plugin allows you to determine and store the actual MIME type of the file analyzed from file content.
plugin :determine_mime_type
plugin :store_dimensions
plugin :pretty_location
plugin :processing
plugin :recache
#The versions plugin enables your uploader to deal with versions,
#by allowing you to return a Hash of files when processing.
plugin :versions
process(:store) do |io, context|
original = io.download
thumbnail = ImageProcessing::MiniMagick
.source(original)
.resize_to_limit!(600, nil)
original.close!
{ original: io, thumbnail: thumbnail }
end
#plugin :versions
#plugin :delete_promoted
#plugin :delete_raw
end
items_controller.rb
#items = Item.includes(:user).page(params[:page] ||= 1).per(8).order('created_at DESC')
render 'index', formats: 'json', handlers: 'jbuilder'
Item/index.json.jbuilder
json.array! #items do |t|
json.id t.id //get the value normally
json.created_at t.created_at //get the value normally
Json.user_id t.user.id //undefined method `id’
json.user_original_img t.user.image_url(:original) //undefined method `image_url'
end
As above, I could not get the value of the model being associated.
By the way, I could check the value correctly with rails console.
Bundle exec rails c
Item.first.user.image_url(:original)
Item Load (1.5ms) SELECT `items`.* FROM `items` ORDER BY `items`.`id` ASC LIMIT 1
User Load (0.7ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> "https://xx.s3.ap-northeast-1.amazonaws.com/store/user/1/image/original-xx”
Item.first.user.id
(19.0ms) SET NAMES utf8mb4, ##SESSION.sql_mode = CONCAT(CONCAT(##sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), ##SESSION.sql_auto_is_null = 0, ##SESSION.wait_timeout = 2147483
Item Load (0.9ms) SELECT `items`.* FROM `items` ORDER BY `items`.`id` ASC LIMIT 1
User Load (0.8ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> 1
Let me know what points I am wrong with.
Thank you for reading my question.
It seems that some items in #items list, doesn't have associated user, or their user_id field is nil. Then item.user would be nil. When you do nil.image_url you get NoMethodError: undefined method 'image_url' for nil:NilClass .
You could add a foreign key constraint between Item and User in your migration, to avoid problems like this:
add_foreign_key :items, :users
NOTE:
Adding the foreign key would still allow empty values. You'd also have to add the following in your migration file to avoid empty values in user_id column:
change_column_null :items, :user_id, false
Thanks to #3limin4t0r for pointing this out.
app/models/user.rb
include UserImageUploader[:image]
has_many :item
should be has_many :items, not terribly confident on this, but this may be the reason you're finding blank columns in your db. A has_many, belongs_to relationship should default to required.
I want to use STI in Rails 4. I already had a model Boilerplate and now I want to inherit a BoilerplateOriginal and a BoilerplateCopy model from it. So I added a column type to my table:
class AddTypeToBoilerplates < ActiveRecord::Migration
def up
add_column :boilerplates, :type, :string
Boilerplate.update_all type: 'BoilerplateOriginal'
change_column :boilerplates, :type, :string, null: false
end
def down
remove_column :boilerplates, :type
end
end
Sadly, this column doesn't seem to be filled by Rails automatically:
[1] a4aa2 » x = Boilerplate.new
=> #<Boilerplate:0x00000101609580> {
:id => nil,
:title => nil,
:type => nil
}
[2] a4aa2 » x.valid?
Boilerplate Exists (0.4ms) SELECT 1 AS one FROM "boilerplates" WHERE "boilerplates"."title" IS NULL LIMIT 1
=> false
[3] a4aa2 » x.errors
=> {
:title => [
[0] "must not be empty"
]
}
[4] a4aa2 » x.title = 'test'
=> "test"
[5] a4aa2 » x.valid?
Boilerplate Exists (0.1ms) SELECT 1 AS one FROM "boilerplates" WHERE "boilerplates"."title" = 'test' LIMIT 1
=> true
[6] a4aa2 » x.save
(0.1ms) begin transaction
Boilerplate Exists (0.2ms) SELECT 1 AS one FROM "boilerplates" WHERE "boilerplates"."title" = 'test' LIMIT 1
SQL (1.4ms) INSERT INTO "boilerplates" ("title") VALUES (?) [["title", "test"]]
SQLite3::ConstraintException: NOT NULL constraint failed: boilerplates.type: INSERT INTO "boilerplates" ("title") VALUES (?)
(0.1ms) rollback transaction
from /Users/josh/.rvm/gems/ruby-2.1.0#a4aa2/gems/sqlite3-1.3.10/lib/sqlite3/statement.rb:108:in `step'
Did I miss something important? I thought Rails simply fills the column :type when it's available?
Well. You are doing it wrong of-course. The type column will be set, only when you will be creating the objects by using any of the child class of the parent Boilerplate. But if you use Boilerplate.new, you need to pass the type value manually. On the other hand, when you will do BoilerplateCopy.new or BoilerplateOriginal.new, the type will be set by ActiveRecord by default for you to the class name of the child class.
Read the official documentation of Single table inheritance.
Active Record allows inheritance by storing the name of the class in a column that by default is named type (can be changed by overwriting Base.inheritance_column). This means that an inheritance looking like this:
class Company < ActiveRecord::Base; end
class Firm < Company; end
class Client < Company; end
class PriorityClient < Client; end
When you do Firm.create(name: "37signals"), this record will be saved in the companies table with type = “Firm”. You can then fetch this row again using Company.where(name: '37signals').first and it will return a Firm object.
I have a query that I run in Rails:
me = User.find(1)
my_groups = me.groups
my_groups can return more than one row, potentially.
Is there a quick and dirty way to use a method to determine if my_groups or me.groups is greater than one?
Maybe something like my_groups.greater_than_one? If not, what would you recommend in determining if the query is return >1 row?
me.groups is essentially another table that is associated with User. It basically shows what "groups" a particular user belongs to.
There needn’t be a method for everything, you can simply compare against size:
me.groups.size > 1
However, ActiveRecord::Relation does have many? which will return true if there is more than one record. From the docs:
Returns true if the collection has more than one record. Equivalent to
collection.size > 1.
class Person < ActiveRecord::Base
has_many :pets
end
person.pets.count #=> 1
person.pets.many? #=> false
person.pets << Pet.new(name: 'Snoopy')
person.pets.count #=> 2
person.pets.many? #=> true
If you only cared about if there are any elements (i.e. >0) there’s any (which is also part of Ruby core’s Enumerable). But beware [nil, false].any? #=> false.
You can get this by:
if me.groups.count > 1 # or me.groups.size > 1 or me.groups.any?
'bla bla...'
else
....
end
But I do recommend to have counter cache in User class.
To do so:
Add a column groups_count to users table
add_column :users, :groups_count, :integer, default: 0
In Group model
belongs_to :user, counter_cache: true
Thus you can achieve your goal by:
if me.groups_count > 1
'bla bla...'
else
....
end
This will reduce the db query
I have a validate_uniqueness_of :field inside my ActiveRecord model. When i do a single create/update it works nicely but i have to do some large batch creation from csv files inside a Transaction
When i am in the transaction the validate_uniqueness_of does not detect the error and the model is saved!
Could it be that the non-unique values are created during the transaction?
The validate methods check before the transaction and then all values are still not present in the table and thus unique.
Edit: Create a index with the unique property turned on for your field and the transaction will fail and thus preventing the addition of non-unique elements.
To do some you should add something this in your migration file
add_index("tablename", "fieldname", { :name => "fieldname_index", :unique => true })
Edit 2: A transaction like this will will give something like a "ActiveRecord::StatementInvalid: Mysql::Error: Duplicate entry '123' for key 1: <sql statement here>" error.
Table.transaction do
i1 = Table.new
i1.fieldname = "123"
i1.save
i2 = Table.new
i2.fieldname = "123"
i2.save
end
validates_uniqueness_of is subject to race conditions, and you still need to have the appropriate unique constraints on your database. You are describing this situation. The link provides a few solutions.