activerecord attribute based on a join? - ruby-on-rails

I'm building a sort of reddit clone to pick up rails again. I have a table posts and a table votes.
Posts:
create_table :posts do |t|
t.belongs_to :user
t.string :title
end
Votes:
create_table :votes do |t|
t.belongs_to :post
t.belongs_to :user
t.string :sort_of_vote
end
I want to retrieve a list of posts with a boolean attribute per post if it is liked by a user or not.
So I would like to something like:
Post.all.first.liked?
I'm thinking of a good way to do this. What I don't want: a query per liked? method call. What would be a good way to achieve this?

Read about Eager Loading Associations, specifically the section on Solution to N + 1 queries problem which uses includes
#posts = Post.includes(:votes)
Now you can create a method on Post like
def liked?
!votes.empty?
end
and call it without forcing a query for each item in the collection.

Is this what you want?
class User
def liked_posts
#liked_posts ||= self.votes.posts
end
def likes?(post)
#liked_posts.include?(post)
end
end
class Post
def liked?(user)
user.liked_posts.include?(self)
end
end
This should retrieve the list of liked posts once and cache it. Then when you call post.liked?(user) or user.likes?(post) it will be able to use the cached data to determine if the user likes that post.

Related

Inserting Pre Determined data in another table when creating record

Hi a Real Rails Rookie here. I am trying to write a basic customer mgmt system and when I create a new customer (customer table) I need it to also create 10 sub-records in another table (customer_ownership) with certain predetermined information which will then be updated/modified when we speak to the customer.
I am really struggling with this, do I try and call the sub_record create controller from the create customer controller or do I write a new controller action in the Customer Controller.
Thanks in advance
I think what you want to do is use an active record callback to perform the work you need done thst is associated with data creation.
Or use a service object design pattern to Perform all actions.
Or you can just add the code for the task to be done after create as a method and call the method directly instead of calling it with a callback.
Or this functionality could live on the model. All oth these options could be considered the “Rails Way” depending on who you talk to.
My preferred method would be...
In controllers/my_object_contoller.rb
def create
#object = MyObject.create(my_object_params)
my_private_method
end
private
def my_private_method
# create auxiliary data objects here
end
Also look into ActiveRecord associations
Because there are ways to create two data models that are programmatically linked or associated with one another using foreign_key ids on the DB columns.
Rails offers an excellent api for you to use which I’ve linked to the rails guide for above.
Such an implementation using active record associations might look like this...
# 'app/models/user.rb
class User < ActiveRecord::Base
has_one :address
...
end
# 'app/models/address.rb'
class Address < ActiveRecord::Base
belongs_to :user
...
end
# 'db/migrate/<timestamp>_create_users.rb'
class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :email
t.string :first_name
t.string :last_name
t.timestamps
end
end
end
# 'db/migrate/<timestamp>_create_addresses.rb'
class CreateAddresses < ActiveRecord::Migration[5.2]
def change
create_table :addresses do |t|
t.references :user, null: false, foreign_key: true
t.string :house_number, null: false
t.string :streen_name
t.string :city
t.string :state
t.string :zip_code
t.timestamps
end
end
end
This give us a new way to manipulate our data. If we create a new user and the extra data we want to add is the users address. Then we can just collect all this data in a form and then user the methods that including the has_one helper gives us. For example...
def create
#user = User.new(params[:user])
#address = Address.new(params[:address])
#user.address = #address
#user.save
end
Of course this is all pseudo code so you should really dive into to active record association link I placed above

Is there a way to check for empty arrays returned from ActiveRecord queries?

I want to write an activerecord query that behaves something like this:
User.joins(:comment).where(comments: {replies: []})
which returns Users where their comments have no replies.
User.first.comment.first.replies => [] returns an empty array
I need this to be an activerecord relation only, so using ruby code won't work. Is there a way to check for empty arrays using activerecord?
Edit:
Example schema
create_table "users", force: :cascade do |t|
t.string "email",
t.string "password"
end
create_table "comments" do |t|
t.string "content"
end
create_table "replies" do |t|
t.string "content"
end
User.rb
class User < ActiveRecord::Base
has_many :comments
end
Comment.rb
class Comment < ActiveRecord::Base
belongs_to :user
has_many :replies
end
Reply.rb
class Reply < ActiveRecord::Base
belongs_to :comment
belongs_to :user
end
These are just example models but this should illustrate the problem with models associated this these.
So if a user(with id of 1) has made 3 comments, and only 1 of those 3 comments has a reply on it, how would I get the other 2 comments in an activerecord relation?
You can make design more simple by using counter_cache
Add counter_cache in replies
class Reply < ActiveRecord::Base
belongs_to :comment, counter_cache: true
belongs_to :user
end
Add counter column in comment table
create_table "comments" do |t|
t.string "content"
t.integer "replies_count"
end
Simple use condition for replies_count
User.joins(:comment).where(comments: {replies_count: 0})
For more info refer to rails doc
You need to use a LEFT JOIN from comments to replies which will return all comments regardless of whether or not there was a reply for every comment. The usual join in rails, i.e. join executed when you do #comment.replies or User.joins(comments: :replies) is an INNER JOIN. ActiveRecord doesn't have a nifty DSL for writing left join queries, but does allow them in the following way:
# Find all users who have at least one comment with no replies
User.joins(:comments)
.joins("LEFT JOIN replies ON replies.comment_id = comments.id")
.where("replies.id IS NULL")
# Find all comments for a user that don't have replies
#user.comments
.joins("LEFT JOIN replies ON replies.comment_id = comments.id")
.where("replies.id IS NULL")
I included the two snippets because in your question it was unclear if you wanted to find the users or the comments for a certain user with no replies.
The where("replies.id IS NULL") is the filter which finds every comment in the LEFT JOIN with no matching replies, and delivers the result you want. This is a very common SQL trick worth keeping in mind for future data digging.

How can I build a flagging system for posts?

I've been trying to figure out the best way to build out a user flagging system in rails 3.1. I experimented with the make_flaggable gem, but it didn't give me what I needed.
I'm using devise for my user model and I have a Post model that belongs to the user. I need to have the ability to retrieve a list of all posts that have been flagged from the admin side of the site. So far, I've had difficulty obtaining that.
I'm uncertain about which type of relationship I would need to use between a Flag model and the Post/User model. I've been reading up on Polymorphic relationships and that is looking promising.
Any ideas or feedback would be much appreciated!
It's very easy to roll your own solution. I would do it this way.
class User
has_many :flags
end
class Post
has_many :flags
end
class Flag
belongs_to :user
belongs_to :post
end
You can get posts that have been flagged by going through the flag model or the post model.
# since a post has many flags
# you will get duplicates for posts flagged more than once
# unless you call uniq
flagged_posts = Post.joins(:flags).uniq
Or through the flags model:
flags = Flag.includes(:post).uniq
flags.each do |flag|
puts flag.post
end
To ensure you don't get duplicate flags on the same post from the same user I would add a uniq index in the migration:
def change
create_table :flags do |t|
t.belongs_to :user, null: false
t.belongs_to :post, null: false
t.timestamps
end
add_index :flags, [:user_id, :post_id], unique: true
end
Maybe I'm misunderstanding what you're trying to do, but why not just add a column to your Posts table called "flagged?" Then you can just do User.posts(:where=> :flagged=>true).

Rails Theory - No Database Dependencies?

I was under the impression that with Rails you're not supposed to define any dependencies in the database, but rather just use your has_many and belongs_to stuff to define relationships. However, I'm going through the rails guide, and it has the following.
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.string :commenter
t.text :body
t.references :post
t.timestamps
end
add_index :comments, :post_id
end
end
I thought this wasn't okay...? I'm trying to do something like a comment field that creates a new instance each time you call the show method, but I think without these "references" and "add_index," it's not storing the post_id in the comment row.
All this migration does is create post_id and tells the database that it should index this column (improves performance)
t.references :post is basically the same as t.integer :post_id so, yes, it is storing the post_id in the comment. You'll still need to define your relationships in your models.
You are actually wrong on the philosophy.
Rails magic is good, only when backed at the DB level by actual foreign keys.
The docs clearly state this
Rails magic comes in, when you have correctly named your foreign keys, so that it can use the convention to figure out the associations.
What's wrong with expressing relationships within the ORM, that's where it's supposed to be done. I believe you are getting mixed up between db vendor specifics such as foreign key constraints and relationships.
class Comment < ActiveRecord::Base
attr_accessible :post, :post_id
belongs_to :post
end
class Post < ActiveRecord::Base
has_many :comments
end
class CommentsController < ApplicationController
def create
#comment = Comment.create(params[:comment]) # where params[:comment] = {post_id: 1, message: ''}
#post = comment.post
respond_with(#comment)
end
end

Rails Undefined method for ni:NilClass Error?

I have a rails app where I have has_many and belongs_to association. But I am having problems while trying to retrieve the elements. My Models are Events, Comments and Votes.
Events has multiple Comments and Votes. Of Course Comments and Voted belong_to one event.
My schema is
create_table "events",
t.string "etime"
t.integer "eid"
end
create_table "votes",
t.integer "eventid"
t.string "userid"
t.integer "event_id"
end
Associations:
class Event < ActiveRecord::Base
has_many :comments
has_many :votes
end
class Votes < ActiveRecord::Base
belongs_to :event
end
I am trying to view all the votes for the events. The controller action is:
#events = Event.all
#events.each do |event|
event.votes.each do |vote|
respond_to do |format|
format.html
end
end
end
View and error line:
<%= #vote.userid %>
I get an error "Undefined Method "userid" for vote". In my controller, I render my eventid and it worked. So it seems to be a problem when I do it in the view..
Any idea What I could be doing wrong? I am completely lost on this one
Might be as simple as using event_id instead of eventid.
It's always worth it to run rails console and play around in there. You can usually autocomplete methods.
v = Vote.new
v.event<tab to get options>
Somewhere you have got it all wrong, your table migrations, as in your foreign_key names are wrong, so are your primary keys.
In standard cases, all models have primary keys as id, just id.
For associating with other model, for example, a belongs_to :event association your vote model needs a , event_id column. These are rails standards
Lets assume, your associations /columns are correct, The error that you get is because your #vote object is nil , in your controller you need to assign/initialize the #vote variable to use it the view which you don't seem to be doing.

Resources