I'm working on my first Rails(3) App, and looking to seed a bunch of data.
The issue I'm having is that I want to seed some models that have a has_and_belongs_to_many relationship with other models I've just seeded. I'm doing what seems right, but I'm not getting the results I'm expecting.
I have an Asana model (simplified):
class Asana < ActiveRecord::Base
has_and_belongs_to_many :therapeutic_foci
end
and the TherapeuticFocus model:
class TherapeuticFocus < ActiveRecord::Base
has_and_belongs_to_many :asanas
end
In my db/seeds.rb, I create some TherapeuticFoci:
tf = TherapeuticFocus.create([
{:name => 'Anxiety'},
{:name => 'Asthma'},
{:name => 'Fatigue'},
{:name => 'Flat Feet'},
{:name => 'Headache'},
{:name => 'High Blood Pressure'},
{:name => 'Stress'} ])
Then create an Asana:
asanaCreate = Asana.create!([
{ :english_name => 'Mountain Pose',
:traditional_name => 'Tadasana',
:pronunciation => 'TadaSANA',
:deck_set => 'Basic',
:type => 'Standing',
:therapeutic_foci => TherapeuticFocus.where("name in ('Stress','Flat Feet')")}
])
The result is that the TherapeuticFocus models are created, the Asana is created, but it doesn't create the relationships to the TherapeuticFocus models. The resulting array is empty.
If I run
TherapeuticFocus.where("name in ('Stress','Flat Feet')")
in the rails console, I get the expected two records:
irb(main):010:0> TherapeuticFocus.where("name in ('Stress','Flat Feet')")
=> [#<TherapeuticFocus id: 6, name: "Flat Feet", description: nil, created_at: "2010-10-11 01:48:02", updated_at: "2010-10-11 01:48:02">,
#<TherapeuticFocus id: 19, name: "Stress", description: nil, created_at: "2010-10-11 01:48:02", updated_at: "2010-10-11 01:48:02">]
So, how does one do this?
Or, is there a better way to do this?
Thanks!
POST ANSWER:
I had already added the inflection:
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'focus', 'foci'
end
My migration for the join tables looks like:
create_table :asanas_therapeutic_foci, :id => false do |t|
t.references :asana, :therapeutic_focus
end
I'll try changing this to t.belongs_to instead of t.references and see if that works.
Did you register the pluralization for “focus”? It is not defined by default, so you will need to define it (usually in config/initializers/inflections.rb):
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'focus', 'foci'
end
You also need to make sure your migration has defined the correct join table for the HABTM association. Here is the pertinent part of the “up” migration that I used:
create_table :asanas do |t|
t.string :english_name
t.string :traditional_name
t.string :pronunciation
t.string :deck_set
t.string :type
end
create_table :therapeutic_foci do |t|
t.string :name
end
create_table :asanas_therapeutic_foci, :id => false do |t|
t.belongs_to :asana
t.belongs_to :therapeutic_focus
end
I used the models as you quoted.
With those bits in place, I was able to load your seed definitions.
Related
In a Rails ( 4.1.5 / ruby 2.0.0p481 / win64 ) application I have a many-to-many relationship between Student and Course and a join model StudentCourse which represents the association, which has an additional attribute called "started", which is set by default on "false".
I also have added an index in the join table made of the student_id and the course_id, and set a unique check on that, like this
t.index [:student_id, :course_id], :unique => true, :name => 'by_student_and_course'
Now I see that associations are created by either doing:
Student.first.courses.create(:name => "english")
or
Course.first.students << Student.first
This is fine and it's the expected behaviour, I suppose.
What I am looking after is the correct way to get and set the "started" attribute.
I am seeing an odd behaviour when accessing that attribute from the other models and not straight from the join model.
s = Student.create
c = Course.create(:name => "english")
s.student_courses.first
=> | "english" | false | # (represented as a table for practicity)
s.student_courses.first.started = true
=> | "english" | true |
s.save
=> true
Ok this looks like it has been saved but when I loot ak:
StudentCourse.first
=> | 1 | 1 | false |
So it is set on true if I go through the student nested attributes, but it's still false in the join model. I also tried doing "reload!" but it makes no difference and they will mantaint their own different value.
If something is going so bad that values are not actually persisted I should be told instead of getting "true" when saving, because otherwise how bad could be the consequences of this ? What am I missing here?
Anyway, if I try modifying the "started" attribute on the join model directly, I meet another kind of problem:
StudentCourse.first.started = true
StudentCourse Load (1.0ms) SELECT "student_courses".* FROM "student_courses" LIMIT 1
=> true
StudentCourse.first.started
=> false
It has not changed!
StudentCourse.find_by(:student_id => "10", :course_id => "1").started = true
=> true
StudentCourse.find_by(:student_id => "10", :course_id => "1").started
=> false
Same as before.. I try with:
StudentCourse.find(1).started = true
ActiveRecord::UnknownPrimaryKey: Unknown primary key for table student_courses in model StudentCourse.
Then with:
sc = StudentCourse.first
sc.started = true
=> true
sc
=> | 1 | 1 | true |
seems great but when saving:
sc.save
(0.0ms) begin transaction
SQL (1.0ms) UPDATE "student_courses" SET "started" = ? WHERE
"student_courses"."" IS NULL [["started", "true"]]
SQLite3::SQLException: no such column: student_courses.: UPDATE
"student_courses" SET "started" = ? WHERE "student_courses"."" IS NULL
(1.0ms) rollback transaction ActiveRecord::StatementInvalid:
SQLite3::SQLException: no such column: student_courses.: UPDATE
"student_courses" SET "started" = ? WHERE "student_courses"."" IS
NULL from
C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/sqlite3-1.3.9-x64-mingw32/lib/sqlite3/database.rb:91:in
`initialize'
So I think this all has to do with not having a primary key in
join-table?
But I am not sure enough on how to use it and if that'd represent a
good practice for the case I am trying to solve ?
Also, if this is the problem, why then I don't get the same warning
here when I save the student after I do
s.student_courses.first.started = true, as shown in the examples
above?
Code
student.rb
class Student < ActiveRecord::Base
has_many :student_courses
has_many :courses, :through => :student_courses
end
course.rb
class Course < ActiveRecord::Base
has_many :student_courses
has_many :students, :through => :student_courses
end
student_course.rb
class StudentCourse < ActiveRecord::Base
belongs_to :course
belongs_to :student
end
schema.rb
ActiveRecord::Schema.define(version: 20141020135702) do
create_table "student_courses", id: false, force: true do |t|
t.integer "course_id", null: false
t.integer "student_id", null: false
t.string "started", limit: 8, default: "pending", null: false
end
add_index "student_courses", ["course_id", "student_id"], name: "by_course_and_student", unique: true
create_table "courses", force: true do |t|
t.string "name", limit: 50, null: false
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "students", force: true do |t|
t.string "name", limit: 50, null: false
t.datetime "created_at"
t.datetime "updated_at"
end
end
create_join_table.rb (migration for join table)
class CreateJoinTable < ActiveRecord::Migration
def change
create_join_table :courses, :students, table_name: :student_courses do |t|
t.index [:course_id, :student_id], :unique => true, :name => 'by_course_and_student'
t.boolean :started, :null => false, :default => false
end
end
end
Ok I finally got what was going on here:
If you create a join table in a migration using #create_join_table, this method will not create the default primary key called "id" (and not add an index for it) which is what rails does by default when using #create_table.
ActiveRecord needs a primary key to build its queries, because it is the column that it will be used by default when doing things like Model.find(3).
Also if you think you can get around this by doing something like StudentCourse.find_by(:course_id => "1", :student_id => "2").update_attributes(:started => true) [0] it will still fail, because after the record it's found, AR will still try to update it looking at the "id" of the record it found.
Also StudentCourse.find_by(:course_id => "1", :student_id => "2").started = true will retrun true but of course it is not saved until you call #save on it. If you assign it to a var relationship and then you call relationship.save you will see it will fail to save for the above reasons.
[0]
In the join table I didn't want duplicate records for a "student_id" and "course_id" so in the migration I had explicitely added a unique constraint for them (using unique index).
This led me to think that I did not need anymore a primary key to uniquely identify a record, because I had those two values... I thought that adding an index on them was enough for they to work as a primary key... but it is not. You need to explicitely define a primary-key when you are not using the default "id" one.
Also turns out that Rails does not support composite primary keys and so even if I wanted to add a primary key build on those two values (so making them primary-key and unique-index, like default rails "id" works) it would have not been possible.
A gem for that exists: https://github.com/composite-primary-keys/composite_primary_keys
So, end of the story, the way I fixed it was simply adding t.column :id, :primary_key to the migration for the join table creation. Also I could have not created the join table with #create_join_table but instead using just #create_table (which would create an "id" automatically").
Hope this helps someone else.
Also this answer to another question was very helpful, thank you #Peter Alfvin !
OK, it appears that you don't have a primary key (we are getting confirmation shortly) in your join table. You do need to have a primary key when trying to access the join table.
I would suggest your migration be:
class CreateStudentCourses < ActiveRecord::Migration
def change
create_table :student_courses do |t|
t.references :course
t.references :student
t.boolean :started, default: false
t.timestamps
t.index [:student_id, :course_id], :unique => true, :name => 'by_student_and_course'
end
end
end
The model definitions look good, so that would be the only change I can see that needs to be made.
After that, doing what you have been doing should work correctly. You would create the join and then access it after the creation. If you want to assign the boolean to true upon creation, you would need to create the record through the StudentCourse model with the information you need (student_id, course_id and started = true) instead of through either association.
StudentCourse.create(course_id: course.id, student_id: student.id, started: true)
s = Student.create
c = Course.create(:name => "english")
s.student_courses.first.started = true
s.save
I think the clue here is in the first line that you posted (represented above). s is an instance of the student and when you call s.save then you're asking the student to save any changes to its attributes. There are not any changes to save, however, because you made a change to an association.
You have a couple of options. If you prefer the direct access approach from your code snippet then the following should work.
s = Student.create
c = Course.create(:name => 'english')
s.courses << c
s.student_courses.first.update_attributes(:started => true)
Another alternative would be to use the accepts_nested_attributes_for macro to expose the started attribute from the student perspective.
class Student
has_many :student_courses, :inverse_of => :student
has_many :courses, :through => :student_courses
accepts_nested_attributes_for :student_courses
end
s = Student.create
c = Course.create(:name => 'english')
s.courses << c
s.update_attributes(:student_courses_attributes=>[{:id => 1, :started => true}])
How can I write the following code in rails 3 without using the has_many_polymorphs plugin?
has_many_polymorphs :listings, :from => [:my_properties, :friend_properties, :public_properties, :private_properties, :job_listings, :truck_listings, :open_listings, :sale_listings], :through => :group_listings
This video help me allot to make this #154 Polymorphic Association
in key on your Polymorphic table you need a ID of the record you want to link to and the name of the table you want to link to.
I usual do it like:
Migration
create_table "cards", force: true do |t|
t.string '....', default: ""
t.integer "accountable_id"
t.string "accountable_type", default: ""
end
Cards Model
belongs_to :accountable, :polymorphic => true
Others Model
has_many :cards, :as => :accountable
I hope that this helps
I'm newbie to Rails.
I have two models Category and Product as follows:-
class Category < ActiveRecord::Base
attr_accessible :type
has_many :products
end
class Product < ActiveRecord::Base
attr_accessible :category_id, :color, :price, :title
belongs_to :category
end
And my schema.rb is as follows:-
ActiveRecord::Schema.define(:version => 20130725220046) do
create_table "categories", :force => true do |t|
t.string "type"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "products", :force => true do |t|
t.integer "category_id"
t.decimal "price", :precision => 10, :scale => 0
t.string "title"
t.string "color"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
end
In Rails console I created two products with two products with the Product.create command
[#<Product id: 1, category_id: 1, price: 500, title: "shirt", color: "blue", `created_at: "2013-07-25 22:04:54", updated_at: "2013-07-25 22:04:54">, #<Product id: 2, category_id: 1, price: 600, title: "tees", color: "black", created_at: "2013-07-25 22:05:17", updated_at: "2013-07-25 22:05:17">]`
And created two Categories with the Category.create command in console
<Category id: 1, type: "clothing", created_at: "2013-07-25 22:03:54", updated_at: "2013-07-25 22:03:54"><Category id: 2, type: "footwear", created_at: "2013-07-25 22:04:02", updated_at: "2013-07-25 22:04:02">
Now, Product.all works fine but Category.all gives
ActiveRecord::SubclassNotFound: The single-table inheritance mechanism failed to locate the subclass: 'clothing'. This error is raised because the column 'type' is reserved for storing the class in case of inheritance. Please rename this column if you didn't intend it to be used for storing the inheritance class or overwrite Category.inheritance_column to use another column for that information.
What's wrong in there? I want to make a relationship between Category and Product like
a category has_many products and products belongs_to a category.
type is restricted word, you can't use it as a column name in ActiveRecord models (unless you're doing STI).
Try using inheritance_column, then type will no longer be reserved:
class Category < ActiveRecord::Base
self.inheritance_column = :foo
attr_accessible :type
has_many :products
end
class Product < ActiveRecord::Base
attr_accessible :category_id, :color, :price, :title
belongs_to :category
end
Just came across this post while trying to find a solution to my own problem with this type reserve word so maybe this can help someone else.
Changing the name of the column isn't an easy solution for me because I'm working on a existing complete system migrating to active record from mongodb.
I found adding self.inheritance_column = :_type_disabled to the model with the offending column name fixed the error which I found here.
Disable STI
I had to add “self.inheritance_column” as opposed to simply
“inheritance_column” to get this to work. Code example
class MyModel < ActiveRecord::Base # disable STI
self.inheritance_column = :_type_disabled end
I know that we're not using any inheritance so I think it's fine for me to do this but I don't know what other implications there could be because of it. If anyone else has any opinions on this I'd be interested to know.
If you can just change the column name and avoid doing this then that's a much better solution I'm sure.
You can set inside your model self.inheritance_column = :_sti_disabled (or some other name) if you are not using STI for current AR model
if anyone is actually trying to pull off STI, try adding a new class that inherits from the parent, and the error message in question should go away.
class YourSubclass < Category
end
If you found this question because you encountered the same error, and you are actually using Rails STI, but got the error because you were trying to rename the inherited subclasses, it's probably because you have old data in the type column and forgot to update it. Then this rake task might help:
update_category_type_column_with_new_subclass_names.rake
# This will be run when running `rake categories:update_type_column_with_new_subclass_names`
namespace :categories do
desc 'Run through the categories with values in the type column and rename those according to the new subclass names: CategorySubclass1 -> CategorySubclass1NewName, CategorySubclass2 -> CategorySubclass2NewName, and CategorySubclass3 -> CategorySubclass3NewName . Run this rake task without arguments.'
task update_type_column_with_new_subclass_names: :environment do
Category.inheritance_column = :_type_disabled # to avoid getting error when using the type column: "ActiveRecord::SubclassNotFound: The single-table inheritance mechanism failed to locate the subclass: 'CategorySubclass1'"
categories = Category.where("type <> '' AND type IS NOT NULL")
categories.each do |a|
begin
case a.type
when 'CategorySubclass1'
a.type = 'CategorySubclass1NewName'
a.save!
when 'CategorySubclass2'
a.type = 'CategorySubclass2NewName'
a.save!
when 'CategorySubclass3'
a.type = 'CategorySubclass3NewName'
a.save!
end
rescue StandardError => e
puts '---'
puts 'Error trying to set :type for category with :'
puts 'id and url: '
puts ' ' + a.id.to_s
puts ' ' + a.type
puts 'Error message: '
puts e.message
puts '---'
end
end
Category.inheritance_column = :type # switch on the Rails STI after you've made your updates to the DB columns
end
end
EDIT3:
To wit, DO NOT put migration code in your model.rb files!!!
EDIT2: THE QUESTION (?) :
does ANY migration code belong in a model.rb file?
EDIT: Just mentioning extra (system/config/etc) information that I need to share in order to get a good answer to this question from someone (even if it's not you) would be greatly appreciated. (1-ups for good tips on stack overflow optimization strategies)
First of all, here is the command prompt activity:
C:\Users\davo\Desktop\RailsProjects\simple_cms>rails c
Loading development environment (Rails 3.2.3)
irb(main):001:0> subject = Subject.find(1)
←[1m←[36mSubject Load (1.0ms)←[0m ←[1mSELECT `subjects`.* FROM `subjects` WHERE `subjects`.`id` = 1
LIMIT 1←[0m
=> #<Subject id: 1, name: "Initial Subject", position: 1, visible: true, created_at:"2012-05-18 01:00:26", updated_at: "2012-05-18 01:11:21">
irb(main):002:0> subject.pages
(Object doesn't support #inspect)
The basic schema is that we have two models here, page.rb and subject.rb. Subject is the parent of Page, as you will see. Here are the two models.
Guide to viewing this code: I think all that is relevant to this problem in these two models are the has_many and belongs_to tags. And I admit, I feel like there should be some foreign keys here. Should there be foreign keys here? Or is that wrong too?
subject.rb
class Subject < ActiveRecord::Base
# attr_accessible :title, :body
has_many :pages
scope :visible, where(:visible => true)
scope :invisible, where(:visible => false)
scope :search, lambda {|query| where(["name LIKE ?", "%#{query}%"])}
end
page.rb
class Page < ActiveRecord::Base
has_many :sections
belongs_to :subject
# attr_accessible :title, :body
create_table "Pages" do |t|
t.string "name"
t.string "permalink"
t.integer "position"
t.boolean "visible?"
end
end
I'm really new at this, so please forgive me if I didn't give you some piece of information that you need. Please let let know what extra information you need, I'm not sure where the error is coming from but I know this is a model (M....VC) issue. 95% on that one.
You have a migration in your model.
create_table "Pages" do |t|
t.string "name"
t.string "permalink"
t.integer "position"
t.boolean "visible?"
end
Should be in ./db/migrate/{timestamp}_create_pages.rb. This file was generated for you if you did rails g model page
You also need a subject_id column to store the relation to subject
class CreatePages < ActiveRecord::Migration
def change
create_table :pages do |t|
t.integer :subject_id
t.string :name
t.string :permalink
t.integer :position
t.boolean :visible?
t.timestamps
end
end
end
I have 2 models. A User and a Task. Here's the code for them both:
class User < ActiveRecord::Base
has_many :tasks
has_many :assigned_tasks, :class_name => 'Task', :foreign_key => 'assigned_user_id'
end
class Task < ActiveRecord::Base
belongs_to :user
belongs_to :assigned_user, :class_name => 'User', :foreign_key => 'assigned_user_id'
end
The schema is quite obvious, but for consistency, this is how it looks:
ActiveRecord::Schema.define(:version => 20110925050945) do
create_table "tasks", :force => true do |t|
t.string "name"
t.integer "user_id"
t.integer "assigned_user_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "users", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
end
I've added a test case for the assigned_tasks relationship. It looks like this:
class UserTest < ActiveSupport::TestCase
test "assigned tasks" do
u1 = User.create(:name => 'john')
u2 = User.create(:name => 'dave')
assert_empty u2.assigned_tasks # LOOK AT ME
task = u1.tasks.create(:name => 'some task', :assigned_user_id => u2.id)
assert_equal 1, u2.assigned_tasks.size
end
end
Now, this test case fails. It fails on the last assertion. If I remove the previous assertion (marked 'LOOK AT ME'), this test passes fine. It also passes fine if I change this line to assert u2.assigned_tasks. Meaning it appears to break when, and only when, empty? is called against u2.assigned_tasks. Where that assertion passes, the following one fails. Here's the failure:
UserTest:
FAIL assigned tasks (0.12s)
<1> expected but was
<0>.
test/unit/user_test.rb:12:in `block in <class:UserTest>'
So, it appears once empty? is called on the original u2.assigned_tasks Array, the task is not actually added/associated with it's assigned user. This however appears to work fine in console.
Apologies if I'm completely overlooking something simple here, but I really can't make any sense of this. Any points in the right direction would be extremely helpful. Thanks
PS: Rails 3.1 with a vanilla application
You need to reload the assigned_tasks, or u2.
# This line causes assigned_tasks to be loaded and cached on u2. Not the calling
# of empty?, but rather the loading of the association.
assert_empty u2.assigned_tasks
# but then you actually make the task here
task = u1.tasks.create(:name => 'some task', :assigned_user_id => u2.id)
# so when this assertion happens, u2 already has an empty set of tasks cached,
# and fails
assert_equal 1, u2.assigned_tasks.size
# however either of these should pass
assert_equal 1, u2.assigned_tasks(true).size
assert_equal 1, u2.reload.assigned_tasks.size
The inverse_of option serves to improve in-memory association behavior, and might also solve your problem (without reloading). Read about that here. It would look something like this (but again I'm not positive it will work in this case):
# on User
has_many :assigned_tasks, ..., :inverse_of => :assigned_user
# on Task
belongs_to :assigned_user, ..., :inverse_of => :assigned_tasks
# in your test you might have to change the task creation to:
u1.tasks.create(:name => 'some task', :assigned_user => u2)