rails: Polymorphic has_many :through broken in Rails 3.2.9?

A fresh Rails app with the following models/migrations misbehaves on Rails 3.2.9:

app/models/user.rb:

class User < ActiveRecord::Base
  has_many :projects, through: :ownerships
  has_many :ownerships, dependent: :destroy
end

app/models/project.rb:

class Project < ActiveRecord::Base
  has_many :owners, as: :ownable, through: :ownerships
  has_many :ownerships, as: :ownable, dependent: :destroy
end

app/models/ownerships.rb:

class Ownership < ActiveRecord::Base
  belongs_to :ownable, polymorphic: true
  belongs_to :owner, :class_name => 'User', foreign_key: 'user_id'
end

Migration:

class CreateAll < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :name
    end

    create_table :projects do |t|
      t.string :name
    end

    create_table :ownerships do |t|
      t.references :user, null: false
      t.references :ownable, polymorphic: true, null: false
    end
  end
end

On Rails 3.2.8:

Loading development environment (Rails 3.2.8)

>> u = User.new {|u| u.name = 'Testuser' }
=> #<User id: nil, name: "Testuser">

>> u.save
   (0.1ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "users" ("name") VALUES (?)  [["name", "Testuser"]]
   (2.8ms)  commit transaction
=> true

>> p = Project.new {|p| p.name = 'Testproject' }
=> #<Project id: nil, name: "Testproject">

>> p.owners << u
=> [#<User id: 7, name: "Testuser">]

>> p.ownerships
=> []

>> p.save
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "projects" ("name") VALUES (?)  [["name", "Testproject"]]
  SQL (2.4ms)  INSERT INTO "ownerships" ("ownable_id", "ownable_type", "user_id") VALUES (?, ?, ?)  [["ownable_id", 4], ["ownable_type", "Project"], ["user_id", 7]]
   (2.6ms)  commit transaction
=> true

On Rails 3.2.9:

Loading development environment (Rails 3.2.9)

>> u = User.new {|u| u.name = 'Testuser' }
=> #<User id: nil, name: "Testuser">

>> u.save
   (0.1ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "users" ("name") VALUES (?)  [["name", "Testuser"]]
   (2.1ms)  commit transaction
=> true

>> p = Project.new {|p| p.name = 'Testproject' }
=> #<Project id: nil, name: "Testproject">

>> p.owners << u
=> [#<User id: 8, name: "Testuser">]

>> p.ownerships
=> [#<Ownership id: nil, user_id: 8, ownable_id: nil, ownable_type: "Project">]

>> p.save
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "projects" ("name") VALUES (?)  [["name", "Testproject"]]
  SQL (3.6ms)  INSERT INTO "ownerships" ("ownable_id", "ownable_type", "user_id") VALUES (?, ?, ?)  [["ownable_id", nil], ["ownable_type", "Project"], ["user_id", 8]]
SQLite3::ConstraintException: ownerships.ownable_id may not be NULL: INSERT INTO "ownerships" ("ownable_id", "ownable_type", "user_id") VALUES (?, ?, ?)
   (2.3ms)  rollback transaction
ActiveRecord::StatementInvalid: SQLite3::ConstraintException: ownerships.ownable_id may not be NULL: INSERT INTO "ownerships" ("ownable_id", "ownable_type", "user_id") VALUES (?, ?, ?)
  from /.../ruby-1.9.3-p327@rails-playground/gems/sqlite3-1.3.6/lib/sqlite3/statement.rb:108:in `step'
  from /.../ruby-1.9.3-p327@rails-playground/gems/sqlite3-1.3.6/lib/sqlite3/statement.rb:108:in `block in each'
  from /.../ruby-1.9.3-p327@rails-playground/gems/sqlite3-1.3.6/lib/sqlite3/statement.rb:107:in `loop'
  from /.../ruby-1.9.3-p327@rails-playground/gems/sqlite3-1.3.6/lib/sqlite3/statement.rb:107:in `each'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/connection_adapters/sqlite_adapter.rb:263:in `to_a'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/connection_adapters/sqlite_adapter.rb:263:in `block in exec_query'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/connection_adapters/abstract_adapter.rb:280:in `block in log'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activesupport-3.2.9/lib/active_support/notifications/instrumenter.rb:20:in `instrument'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/connection_adapters/abstract_adapter.rb:275:in `log'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/connection_adapters/sqlite_adapter.rb:242:in `exec_query'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/connection_adapters/abstract/database_statements.rb:63:in `exec_insert'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/connection_adapters/abstract/database_statements.rb:90:in `insert'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/connection_adapters/abstract/query_cache.rb:14:in `insert'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/relation.rb:66:in `insert'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/persistence.rb:367:in `create'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/timestamp.rb:58:in `create'
... 43 levels...
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/validations.rb:50:in `save'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/attribute_methods/dirty.rb:22:in `save'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/transactions.rb:259:in `block (2 levels) in save'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/transactions.rb:313:in `block in with_transaction_returning_status'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/connection_adapters/abstract/database_statements.rb:192:in `transaction'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/transactions.rb:208:in `transaction'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/transactions.rb:311:in `with_transaction_returning_status'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/transactions.rb:259:in `block in save'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/transactions.rb:270:in `rollback_active_record_state!'
  from /.../ruby-1.9.3-p327@rails-playground/gems/activerecord-3.2.9/lib/active_record/transactions.rb:258:in `save'
  from (irb):8
  from /.../ruby-1.9.3-p327@rails-playground/gems/railties-3.2.9/lib/rails/commands/console.rb:47:in `start'
  from /.../ruby-1.9.3-p327@rails-playground/gems/railties-3.2.9/lib/rails/commands/console.rb:8:in `start'
  from /.../ruby-1.9.3-p327@rails-playground/gems/railties-3.2.9/lib/rails/commands.rb:41:in `<top (required)>'
  from script/rails:6:in `require'
  from script/rails:6:in `<main>'>> 

About this issue

  • Original URL
  • State: closed
  • Created 12 years ago
  • Comments: 51 (29 by maintainers)

Most upvoted comments

@aaronbrethorst I’ve been going through a lot of this as well. It seems a lot of the problems come from join models that have presence validations. You can specify inverse_of on your “joining” models to keep validations happy.

class Thing < ActiveRecord::Base
  has_many :widgetings, :inverse_of => :thing
  has_many :widgets, :through => :widgetings
end

class Widget < ActiveRecord::Base
  has_many :widgetings, :inverse_of => :widget
  has_many :things, :through => :widgetings
end

class Widgeting < ActiveRecord::Base
  belongs_to :thing
  belongs_to :widget
  validates :thing, :widget, :presence => true
end

Thing.new(:widgets => [Widget.new]).valid? # => true, it would complain that the Widgeting is invalid without inverse_of specified

Warning: Its late, and I’m sleepy. Please don’t hold the above against me 😃