I was trying to save nested attributes for technologies with portfolio, I have the following code: portfolio.rb (model)
class Portfolio < ApplicationRecord
has_many :technologies
# do not accept the insertion if name is blank
accepts_nested_attributes_for :technologies,
reject_if: lambda{ |attrs| attrs['name'].blank? }
validates_presence_of :title, :body, :main_image, :thumb_image
include Placeholder
validates_presence_of :title, :body, :main_image, :thumb_image
# class method - custom scope
def self.angulars
where(subtitle: "Angular")
end
# lambda - custom scope
scope :ruby_on_rails_p_items, -> { where(subtitle: "Ruby on Rails") }
# callbackss
after_initialize :set_defaults
def set_defaults
self.main_image ||= Placeholder.image_generator(height:'600', width:'400')
self.thumb_image ||= Placeholder.image_generator(height:'350', width:'200')
end
end
technology.rb model:
class Technology < ApplicationRecord
belongs_to :portfolio
end
Now when I go to the rails console and try to insert multiple attributes:
Portfolio.create!(title: "Web App", subtitle: "asadasd", body: "sadsadas", technologies_attributes: [{name: "Ruby"}, {name: "Rails"}, {name: "Angula"}, {name: "Ionic"}])
It gave me this error:
ActiveRecord::RecordInvalid: Validation failed: Technologies portfolio must exist
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.7/lib/active_record/validations.rb:78:in `raise_validation_error'
Any idea what am I missing here?
Schema for both technology and portfolio:
create_table "portfolios", force: :cascade do |t|
t.string "title"
t.string "subtitle"
t.text "body"
t.text "main_image"
t.text "thumb_image"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "technologies", force: :cascade do |t|
t.string "name"
t.integer "portfolio_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["portfolio_id"], name: "index_technologies_on_portfolio_id", using: :btree
end
COMPLETE ERROR MESSAGE:
ActiveRecord::RecordInvalid: Validation failed: Technologies portfolio must exist
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.7/lib/active_record/validations.rb:78:in `raise_validation_error'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.7/lib/active_record/validations.rb:50:in `save!'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.7/lib/active_record/attribute_methods/dirty.rb:30:in `save!'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.7/lib/active_record/transactions.rb:324:in `block in save!'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.7/lib/active_record/transactions.rb:395:in `block in with_transaction_returning_status'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.7/lib/active_record/connection_adapters/abstract/database_statements.rb:232:in `block in transaction'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.7/lib/active_record/connection_adapters/abstract/transaction.rb:189:in `within_new_transaction'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.7/lib/active_record/connection_adapters/abstract/database_statements.rb:232:in `transaction'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.7/lib/active_record/transactions.rb:211:in `transaction'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.7/lib/active_record/transactions.rb:392:in `with_transaction_returning_status'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.7/lib/active_record/transactions.rb:324:in `save!'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.7/lib/active_record/suppressor.rb:45:in `save!'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.7/lib/active_record/persistence.rb:51:in `create!'
from (irb):3
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/railties-5.0.7/lib/rails/commands/console.rb:65:in `start'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/railties-5.0.7/lib/rails/commands/console_helper.rb:9:in `start'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/railties-5.0.7/lib/rails/commands/commands_tasks.rb:78:in `console'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/railties-5.0.7/lib/rails/commands/commands_tasks.rb:49:in `run_command!'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/railties-5.0.7/lib/rails/commands.rb:18:in `<top (required)>'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.7/lib/active_support/dependencies.rb:293:in `require'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.7/lib/active_support/dependencies.rb:293:in `block in require'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.7/lib/active_support/dependencies.rb:259:in `load_dependency'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.7/lib/active_support/dependencies.rb:293:in `require'
from /Users/mac/Desktop/DevCampPortfolio/bin/rails:9:in `<top (required)>'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.7/lib/active_support/dependencies.rb:287:in `load'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.7/lib/active_support/dependencies.rb:287:in `block in load'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.7/lib/active_support/dependencies.rb:259:in `load_dependency'
from /Users/mac/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.7/lib/active_support/dependencies.rb:287:in `load'
from /Users/mac/.rvm/rubies/ruby-2.3.1/lib/ruby/site_ruby/2.3.0/rubygems/core_ext/kernel_require.rb:55:in `require'
from /Users/mac/.rvm/rubies/ruby-2.3.1/lib/ruby/site_ruby/2.3.0/rubygems/core_ext/kernel_require.rb:55:in `require'
from -e:1:in `<main>'
2.3.1 :004 > Portfolio.create!(title: "Web App", subtitle: "asadasd", body: "sadsadas", main_image: 'some value', thumb_image: 'some image')
Making the belongs_to :portfolio association optional is not solving the problem. All you are doing by passing in optional: true is skipping validation, which is not useful if you actually want to set up a database schema with Model associations.
The underlying cause of the problem is that Active Record is attempting to create a Technology object before the Portfolio object has been committed to the database. accepts_nested_attributes_for is special in that it creates an instance of the associated model (Technology in this case) through the parent model (Portfolio). To do this Active Record needs to know about the relation between the two models, and it must also have a foreign key to tie the associated Technology object to a specific Portfolio.
If you were to create a Portfolio object first, and then a Technology object separately you wouldn't have a problem:
portfolio = Portfolio.create!(title: "Web App", subtitle: "asadasd", body: "sadsadas")
Technology.create!(name: 'aoeifjeao', portfolio_id: portfolio.id)
=> #<Technology id: 10, name: "aoeifjeao", portfolio_id: 17, created_at: "2019-12-07 03:54:47", updated_at: "2019-12-07 03:54:47">
The reason this works is clear if you inspect the SQL queries generated by the Technology.create code:
(0.4ms) BEGIN
Portfolio Load (0.7ms) SELECT "portfolios".* FROM "portfolios" WHERE "portfolios"."id" = $1 LIMIT $2 [["id", 17], ["LIMIT", 1]]
SQL (1.6ms) INSERT INTO "technologies" ("name", "portfolio_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["name", "aoeifjeao"], ["portfolio_id", 17], ["created_at", "2019-12-07 03:54:47.668662"], ["updated_at", "2019-12-07 03:54:47.668662"]]
(41.0ms) COMMIT
Note that the first thing that happens is Active Record looks for a Portfolio from the database with a specific id. It then inserts a new record into the technologies table with the same portfolio_id.
This is what happens when you create a model with an association using Active Record. It looks for the parent model in the database first using the id you specified, and then that becomes the foreign key of the child model.
Returning to your code, when you call Portfolio.create, what you are actually trying to do is create both the parent and child objects, plus set up their association, all before any records are saved to the database.
What is actually happening is that Portfolio.create gets broken up into two steps. First Portfolio.new is called, which instantiates (but doesn't save) the Portfolio Object:
portfolio = Portfolio.new(title: "Web App", subtitle: "asadasd", body: "sadsadas", technologies_attributes: [{name: "Ruby"}])
=> #<Portfolio id: nil, title: "Web App", subtitle: "asadasd", body: "sadsadas", main_image: "http://placehold.it/600x400", thumb_image: "http://placehold.it/350x200", created_at: nil, updated_at: nil>
Note that our nested attributes have already been used to instantiate a Technology Object through the Portfolio association, but the foreign key portofolio_id is nil:
portfolio.technologies
=> #<ActiveRecord::Associations::CollectionProxy [#<Technology id: nil, name: "Ruby", portfolio_id: nil, created_at: nil, updated_at: nil>]>
Next, Portfolio.save is called, which throws ActiveRecord::RecordInvalid validation error. This is telling you that the Technology object requires a Portfolio association, which it does not have because portfolio_id is still nil.
When setting up belongs_to and has_many relationships, Active Record attempts to automatically guess the inverse association based on heuristics, typically by looking at the table names. It's usually pretty good at it, but sometimes you need to be explicit and declare the inverse_of relationships between the two models.
# portfolio.rb
class Portfolio < ApplicationRecord
has_many :technologies, inverse_of: :portfolio
end
# technology.rb
class Technology < ApplicationRecord
belongs_to :portfolio, inverse_of: :technologies
end
You have validation on Portfolio
validates_presence_of :title, :body, :main_image, :thumb_image
And you are passing only :title, :subtile, :body but not :main_image, :thumb_image
Portfolio.create!(title: "Web App", subtitle: "asadasd", body: "sadsadas", technologies_attributes: [{name: "Ruby"}, {name: "Rails"}, {name: "Angula"}, {name: "Ionic"}])
Above code expecting saving of Portfolio should be successful, so it can use portfolio_id in Technology, but it isn't happening here you need to pass in all required parameter to Portfolio.create!
Portfolio.create!(title: "Web App", subtitle: "asadasd", body: "sadsadas", main_image: 'some value', thumb_image: 'some image',
technologies_attributes: [{name: "Ruby"}, {name: "Rails"}, {name: "Angula"}, {name: "Ionic"}])
Update:
As you are using Rails 5.x, In Rails 5.x (onwards) all belongs_to association validates(belongs_to association) presence true by default, add optional: true to your Technology Model. Read more about this
class Technology < ApplicationRecord
belongs_to :portfolio, optional: true
end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With