I have a post that has many comments. Comments have a body and a title
=> #<ActiveRecord::Associations::CollectionProxy [#<Comment id: 1, author: "jack", body: "how do you like dem apples?", post_id: 1, created_at: "2016-09-29 02:11:00", updated_at: "2016-09-29 02:11:00">]>
2.3.0 :005 > Post.first.comments
Post Load (0.5ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT 1
Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Comment id: 1, author: "jack", body: "how do you like dem apples?", post_id: 1, created_at: "2016-09-29 02:11:00", updated_at: "2016-09-29 02:11:00">]>
2.3.0 :006 > Post.first.comments.body
NoMethodError: Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 1]]
undefined method `body' for #<Comment::ActiveRecord_Associations_CollectionProxy:0x007f9bef0a33a8>
In the code above you can see that I try to get the body attribute from the post that has a comment, but I get a no method exception. How do I retrieve the associated objects data in these types of situations?
1) You get error because you are calling body on the collection of comments, not a single instance of Comment class.
2) To get it working:
# select the comment, which's body you want to get
Post.first.comments.first.body
Post.first.comments is a collection, you can treat it as an array and map it, for example, to get all comments' bodies:
# would return all bodies of all comments, that belongs to the `Post.first`
Post.first.comments.pluck(:body)
Always read exceptions messages carefully.
Related
What the difference between:
self.implicit_order_column = 'id'
and
default_scope { order('id ASC') }
self.implicit_order_column lets you use another column then the primary key as the implicit ordering column. This effects the way that methods like .first and .last work:
User.class_eval do
self.implicit_order_column = 'created_at'
end
User.first
# => SELECT "users".* FROM "users" ORDER BY "users"."updated_at" ASC LIMIT $1 [["LIMIT", 1]]
User.last
# => SELECT "users".* FROM "users" ORDER BY "users"."updated_at" DESC LIMIT $1 [["LIMIT", 1]]
Setting self.implicit_order_column = 'id' is utterly pointless since the default is the primary key column anyways. The implicit_order_column is of course not used if you provide an explicit order. It does not actually change any other scopes spawned off the class.
default_scope on the other hand tacks on a default scope to any scopes that you spawn off the class.
irb(main):001:0> User.all
(0.5ms) SELECT sqlite_version(*)
User Load (0.1ms) SELECT "users".* FROM "users" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<User id: 1, role: "admin", created_at: "2020-11-08 19:31:31", updated_at: "2020-11-08 19:31:47">]>
irb(main):002:1* User.class_eval do
irb(main):003:1* default_scope { order(id: :asc) }
irb(main):004:0> end
=> [#<Proc:0x00000000043703a8 (irb):3>]
irb(main):005:0> User.all
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<User id: 1, role: "admin", created_at: "2020-11-08 19:31:31", updated_at: "2020-11-08 19:31:47">]>
irb(main):006:0>
The difference here is not immediately apparent. But if you consider that in the SQL world a query with no order clause will not return the records in a determinate order (its implementation dependent) and here we are actually getting the records in a determinate order. Om many RDBMS the results will be hard to destinguish as they might return the records in the order they are modified (if they feel like it).
Seems brilliant until you realize how non-intuitive default_scope is and how many bugs it leads to down the line.
irb(main):006:0> User.all.order(:created_at)
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC, "users"."created_at" ASC LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<User id: 1, role: "admin", created_at: "2020-11-08 19:31:31", updated_at: "2020-11-08 19:31:47">]>
irb(main):007:0>
Or this example:
irb(main):001:1* User.class_eval do
irb(main):002:1* default_scope { where(admin: true) }
irb(main):003:0> end
=> [#<Proc:0x0000000002bde460 (irb):2>]
irb(main):004:0> User.new
(0.6ms) SELECT sqlite_version(*)
=> #<User id: nil, role: "visitor", created_at: nil, updated_at: nil, admin: true>
Yikes! default_scope is thus widely considered evil.
See:
https://github.com/rails/rails/pull/34480
https://rails-bestpractices.com/posts/2013/06/15/default_scope-is-evil/
https://www.reddit.com/r/rails/comments/ha6gqj/default_scope_is_evil/
I have a user and team models joined by a membership model.
One user can have many teams and each team can have many users etc.
class User < ApplicationRecord
has_many :memberships
has_many :teams, through: :memberships
End
class Team < ApplicationRecord
has_many :memberships
has_many :users, through: :memberships
End
class Membership < ApplicationRecord
belongs_to :user
belongs_to :team
end
I am looking for a way to simply create a memberships record however am missing something:
2.4.0 :026 > t = Team.last
Team Load (1.2ms) SELECT "teams".* FROM "teams" ORDER BY "teams"."id" DESC LIMIT $1 [["LIMIT", 1]]
=> #<Team id: 42129, name: "Reds", description: "A good team", created_at: "2017-05-18 05:05:09", updated_at: "2017-05-18 05:05:09">
2.4.0 :027 > User.first.memberships
User Load (1.7ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]]
Membership Load (1.1ms) SELECT "memberships".* FROM "memberships" WHERE "memberships"."user_id" = $1 LIMIT $2 [["user_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy []>
2.4.0 :028 > User.first.memberships << t
User Load (0.8ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]]
(0.4ms) BEGIN
(0.4ms) ROLLBACK
ActiveRecord::AssociationTypeMismatch: Membership(#23507760) expected, got #<Team id: 42129, name: "Reds", description: "A good team", created_at: "2017-05-18 05:05:09", updated_at: "2017-05-18 05:05:09"> which is an instance of Team(#33684520)
from (irb):28
2.4.0 :029 >
What am I missing here?
Update:
When I create a new memberships record and manually add the fk values I get this error when I try to save:
2.4.0 :037 > m
=> #<Membership id: nil, user_id: 1, team_id: 22641, created_at: nil, updated_at: nil>
2.4.0 :038 > m.save
(0.5ms) BEGIN
(0.4ms) ROLLBACK
NoMethodError: undefined method `class_name' for nil:NilClass
Did you mean? class_eval
from (irb):38
Update2
2.4.0 :022 > user = User.first
User Load (2.7ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> #<User id: 1, created_at: "2017-05-15 08:17:01", updated_at: "2017-05-19 02:54:30">
2.4.0 :023 > team = Team.first
Team Load (2.3ms) SELECT "teams".* FROM "teams" ORDER BY "teams"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> #<Team id: 22641, name: "Reds", description: "This is a good team", created_at: "2017-05-18 01:41:00", updated_at: "2017-05-18 05:05:09">
2.4.0 :027 > Membership.delete_all
SQL (1.9ms) DELETE FROM "memberships"
=> 1
2.4.0 :029 > m=Membership.new(user: user, team: team)
=> #<Membership id: nil, user_id: 1, team_id: 22641, created_at: nil, updated_at: nil>
2.4.0 :031 > m.save
(1.0ms) BEGIN
SQL (2.3ms) INSERT INTO "memberships" ("user_id", "team_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["user_id", 1], ["team_id", 22641], ["created_at", "2017-05-22 03:37:22.803718"], ["updated_at", "2017-05-22 03:37:22.803718"]]
(2.1ms) COMMIT
=> true
2.4.0 :032 > Membership.delete_all
SQL (1.9ms) DELETE FROM "memberships"
=> 1
2.4.0 :033 > m=Membership.new
=> #<Membership id: nil, user_id: nil, team_id: nil, created_at: nil, updated_at: nil>
2.4.0 :034 > m.user_id=user.id
=> 1
2.4.0 :035 > m.team_id=team.id
=> 22641
2.4.0 :036 > m
=> #<Membership id: nil, user_id: 1, team_id: 22641, created_at: nil, updated_at: nil>
2.4.0 :037 > m.save
(0.5ms) BEGIN
User Load (0.9ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
(0.8ms) ROLLBACK
NoMethodError: undefined method `class_name' for nil:NilClass
Did you mean? class_eval
from (irb):37
The first error message you're getting (before the Update) is because ActiveRecord is expecting an instance of Membership, and it's getting a Team. User.first.teams << t should work for that example.
As far as the error you shared in the Update, I didn't encounter that when I tried it, and it seemed to work fine with the relationships you gave.
2.2.3 :009 > user = User.first
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
=> #<User id: 1>
2.2.3 :010 > team = Team.first
Team Load (0.2ms) SELECT "teams".* FROM "teams" ORDER BY "teams"."id" ASC LIMIT 1
=> #<Team id: 1>
2.2.3 :011 > membership = Membership.new(user: user, team: team)
=> #<Membership id: nil, user_id: 1, team_id: 1>
2.2.3 :012 > membership.save
(0.1ms) begin transaction
SQL (1.2ms) INSERT INTO "memberships" ("user_id", "team_id") VALUES (?, ?) [["user_id", 1], ["team_id", 1]]
(2.3ms) commit transaction
=> true
One thing to double check would be in how you're constructing the Membership record, and that there are no pluralization errors in the models. Passing in the actual User and Team instances to Membership.new or Membership.create should work.
So I have two models - Status and Post.
A status can either be: Empty, Half, or Full.
Each post can only have 1 status at any one time, i.e. a post can only be either empty or half, etc. It has to be one of the 3.
However, a status can have many posts. So the Empty status may have 20 posts.
What is the best way to approach this, from an associations perspective?
I was initially thinking that a Post has_one Status. But the issue with that, is that the Status would have to belong_to a Post.
So right now, I have it as:
Status has_many :posts
Post belongs_to :status, counter_cache: true
But whenever I want to assign a status to a post, I have to do it backways and it feels weird.
i.e. I have to do something like:
> g = Post.second
Post Load (0.7ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT 1 OFFSET 1
=> #<Post id: 19, title: "10PP gives you 1 on 1", photo: "1-on-1-Icon.jpg", body: "10PP gives you the real one on one attention you c...", created_at: "2014-08-30 10:48:18", updated_at: "2014-08-30 10:48:18", user_id: 1, ancestry: nil, file: nil, status_id: nil>
2.1.1p76 :010 > half = Status.second
Status Load (0.5ms) SELECT "statuses".* FROM "statuses" ORDER BY "statuses"."id" ASC LIMIT 1 OFFSET 1
=> #<Status id: 2, name: "half", created_at: "2014-08-28 08:04:42", updated_at: "2014-08-28 08:04:42", posts_count: nil>
2.1.1p76 :011 > half.posts << g
(0.1ms) BEGIN
SQL (0.3ms) UPDATE "posts" SET "status_id" = $1, "updated_at" = $2 WHERE "posts"."id" = 19 [["status_id", 2], ["updated_at", "2014-08-30 11:37:16.121245"]]
SQL (0.4ms) UPDATE "statuses" SET "posts_count" = COALESCE("posts_count", 0) + 1 WHERE "statuses"."id" = 2
(0.9ms) COMMIT
Post Load (0.8ms) SELECT "posts".* FROM "posts" WHERE "posts"."status_id" = $1 [["status_id", 2]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Post id: 19, title: "10PP gives you 1 on 1", photo: "1-on-1-Icon.jpg", body: "10PP gives you the real one on one attention you c...", created_at: "2014-08-30 10:48:18", updated_at: "2014-08-30 11:37:16", user_id: 1, ancestry: nil, file: nil, status_id: 2>]>
I would rather be going the other way, i.e. assigning a status to the post.
I feel like this is not the best way to do it, but not sure how else to tackle it.
Also, how would I tackle the PostsController#Create?
Right now, I have just:
#post = current_user.posts.new(post_params)
Which doesn't assign the correct Status object to the current post.
you could try other way with using enum http://edgeguides.rubyonrails.org/4_1_release_notes.html#active-record-enums
class Post < ActiveRecord::Base
enum status: [ :empty, :half, :full ]
end
Post.full
Post.first.empty?
Ok..I hope I am doing something dumb (which is usually the case). I have been humming along, adding cache counters to my existing project, when all of the sudden reset_counters failed for pretty much everything. I checked out an old copy when things were working, and it still failed, so I started on a brand new demo project.
I assume this is my error because it suddenly stopped working..and I don't see anyone else having similar issues.
I am using ruby 1.9.3-p392, and rails 3.2.12.
The counters themselves work on both existing and new projects. you just can't use reset_counters.
So here is the problem on a brand new project:
class Post < ActiveRecord::Base
attr_accessible :name
has_many :comments
end
class Comment < ActiveRecord::Base
attr_accessible :name
belongs_to :post, :counter_cache => true
end
here we can see the counters increment:
irb(main):073:0* post = Post.create(:name => 'i am a post')
(0.3ms) BEGIN
SQL (0.4ms) INSERT INTO `posts` (`comments_count`, `created_at`, `name`, `updated_at`) VALUES (0, '2013-03-14 19:56:53', 'i am a post', '2013-03-14 19:56:53')
(0.5ms) COMMIT
=> #<Post id: 3, name: "i am a post", created_at: "2013-03-14 19:56:53", updated_at: "2013-03-14 19:56:53", comments_count: 0>
irb(main):074:0> post.comments << Comment.new(:name => 'i am a comment')
(0.2ms) BEGIN
SQL (0.3ms) INSERT INTO `comments` (`created_at`, `name`, `post_id`, `updated_at`) VALUES ('2013-03-14 19:57:18', 'i am a comment', 3, '2013-03-14 19:57:18')
Post Load (0.3ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 3 LIMIT 1
SQL (0.3ms) UPDATE `posts` SET `comments_count` = COALESCE(`comments_count`, 0) + 1 WHERE `posts`.`id` = 3
(0.4ms) COMMIT
Comment Load (0.2ms) SELECT `comments`.* FROM `comments` WHERE `comments`.`post_id` = 3
=> [#<Comment id: 3, name: "i am a comment", post_id: 3, created_at: "2013-03-14 19:57:18", updated_at: "2013-03-14 19:57:18">]
irb(main):075:0> post = Post.find(3)
Post Load (0.5ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 3 LIMIT 1
=> #<Post id: 3, name: "i am a post", created_at: "2013-03-14 19:56:53", updated_at: "2013-03-14 19:56:53", comments_count: 1>
running the code I get:
irb(main):001:0> Post.reset_counters(1,:comments_count)
Post Load (0.5ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 1 LIMIT 1
NoMethodError: undefined method `options' for nil:NilClass
from /Users/charles/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/activerecord-3.2.12/lib/active_record/counter_cache.rb:22:in `block in reset_counters'
from /Users/charles/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/activerecord-3.2.12/lib/active_record/counter_cache.rb:19:in `each'
from /Users/charles/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/activerecord-3.2.12/lib/active_record/counter_cache.rb:19:in `reset_counters'
from (irb):1
from /Users/charles/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/railties-3.2.12/lib/rails/commands/console.rb:47:in `start'
from /Users/charles/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/railties-3.2.12/lib/rails/commands/console.rb:8:in `start'
from /Users/charles/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/railties-3.2.12/lib/rails/commands.rb:41:in `<top (required)>'
from script/rails:6:in `require'
from script/rails:6:in `<main>'
You're almost entirely correct. The parameter in reset_counters though should be the object name, not the column name. So in your case you want to:
1.9.3-p392 :005 > Post.reset_counters(1, :comments)
Post Load (0.3ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT 1 [["id", 1]]
(0.1ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 1
(2.3ms) UPDATE "posts" SET "comments_count" = 1 WHERE "posts"."id" = 1
=> true
[1] pry(main)> User.first
User Load (0.4ms) SELECT "users".* FROM "users" LIMIT 1
=> #<User id: 3, email: "chasity#kiehn.net", encrypted_password: "$2a$10$lqsgKvQuz9iSIu/..FMRJu76H9KNhfVz5x9DmxphC0TK...", reset_password_token: ... >
[2] pry(main)> User.find(1)
User Load (12.8ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
=> #<User id: 1, email: "admin#example.com", encrypted_password: "$2a$10$pGDcv0/EgiDk4KcZN9pli.evx5Ni1qOoujQD15HgWH8Y...", reset_password_token: ... >
[3] pry(main)> Product.first
Product Load (0.6ms) SELECT "products".* FROM "products" LIMIT 1
=> #<Product id: 1, name: "Ruby on Rails Jr. Spaghetti", created_at: "2012-01-25 10:13:26", updated_at: "2012-01-25 10:13:26", properties: ...>
This is happening in the console and during runtime and only for the User model. What could cause such behaviour?
You haven't specified an order - the database is free to order the results in any way it wants. It could be primary key order, it could be how things are laid out on disc but without an order clause there are no guarantees.
If you want the first record in a specific order, then make sure you ask for it
User.first is the same as User.all.first, there is no specified order, so the DB returns the list of element in any order. If you want to get the User with smallest id, use User.order("id").first.
If your request returns several elements and you want to pick any of them, you may want to use first without specified order.