Let's say you have the following models:
class User < ActiveRecord::Base
has_many :comments, :as => :author
end
class Comment < ActiveRecord::Base
belongs_to :user
end
Let's say User has an attribute name, is there any way in Ruby/Rails to access it using the table name and column, similar to what you enter in a select or where query?
Something like:
Comment.includes(:author).first.send("users.name")
# or
Comment.first.send("comments.id")
Edit: What I'm trying to achieve is accessing a model object's attribute using a string. For simple cases I can just use object.send attribute_name but this does not work when accessing "nested" attributes such as Comment.author.name.
Basically I want to retrieve model attributes using the sql-like syntax used by ActiveRecord in the where() and select() methods, so for example:
c = Comment.first
c.select("users.name") # should return the same as c.author.name
Edit 2: Even more precisely, I want to solve the following problem:
obj = ANY_MODEL_OBJECT_HERE
# Extract the given columns from the object
columns = ["comments.id", "users.name"]
I don't really understand what you are trying to achieve. I see that you are using polymorphic associations, do you need to access comment.user.name while having has_many :comments, :as => :author in your User model?
For you polymorphic association, you should have
class Comment < ActiveRecord::Base
belongs_to :author, :polymorphic => true
end
And if you want to access comment.user.name, you can also have
class Comment < ActiveRecord::Base
belongs_to :author, :polymorphic => true
belongs_to :user
end
class User < ActiveRecord::Base
has_many :comments, :as => :author
has_many :comments
end
Please be more specific about your goal.
I think you're looking for a way to access the user from a comment.
Let #comment be the first comment:
#comment = Comment.first
To access the author, you just have to type #comment.user and If you need the name of that user you would do #comment.user.name. It's just OOP.
If you need the id of that comment, you would do #comment.id
Because user and id are just methods, you can call them like that:
comments.send('user').send('id')
Or, you can build your query anyway you like:
Comment.includes(:users).where("#{User::columns[1]} = ?", #some_name)
But it seems like you're not doing thinks really Rails Way. I guess you have your reasons.
Related
As I am learning RoR now, I would like to know a more appropriated (rails) way to achieve that the application only shows associated resources.
Right now I have the following models:
class Account < ActiveRecord::Base
has_many :billing_accounts
has_many :addresses
end
class BillingAccount < ActiveRecord::Base
belongs_to :invoice_address,
class_name: "Address",
foreign_key:"invoice_address_id"
end
class Address < ActiveRecord::Base
has_many :billing_accounts
belongs_to :account
end
In my edit.billing_account I have this form:
= simple_form_for([:account, #billing_account]) do |f|
= f.association :invoice_address
I expected that only the associated address will be shwon, but this shows "all" address records in the database (also from other user accounts).
Users only should be able to see account.addresses and for now I do this with:
= f.association :invoice_address, collection: current_user.account.addresses.all
But I am sure there is better way to do this inside the models. For every form I now use current_user.account.MODEL.all but that is not very DRY I think.
So basically what I want is only to use =f.association :invoice_address and BillingAccount should know it only can show the account.addresses.
Suggestions are welcome. Thanks!
You just need to set default_scope for nested models:
class Address < ActiveRecord::Base
default_scope { where(account_id: current_user.account_id) }
But in this case you should define current_user in models
In your case you should use f.simple_fields_for instead of f.association as described here: https://github.com/plataformatec/simple_form/wiki/Nested-Models
class BillingAccount < ActiveRecord::Base
belongs_to :invoice_address,
class_name: "Address",
foreign_key:"invoice_address_id"
accepts_nested_attributes_for :invoice_address
end
View:
= simple_form_for([:account, #billing_account]) do |f|
= f.simple_fields_for :invoice_address do |f_address|
= f_address.input :street
= f_address.input :zipcode
...
Don't forget to build invoice_address of account in a controller if it is needed. For example:
class BillingAccountController < ApplicationController
def new
#billing_account = BillingAccount.new
#billing_account.build_invoice_address
end
Since you're using has_many you can use the plural version of the model name rather than current_user.account.MODEL.all.
Like this:
current_user.account.addresses
or
current_user.account.billing_accounts
It even works the other way with belongs_to:
#address = Address.last
#address.accounts
Try to add conditions to belongs_to association:
class BillingAccount < ActiveRecord::Base
belongs_to :invoice_address,
->(billing_account) { where "account_id = #{billing_account.account_id}" },
class_name: "Address",
foreign_key:"invoice_address_id"
end
I want to be able to use two columns on one table to define a relationship. So using a task app as an example.
Attempt 1:
class User < ActiveRecord::Base
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
end
So then Task.create(owner_id:1, assignee_id: 2)
This allows me to perform Task.first.owner which returns user one and Task.first.assignee which returns user two but User.first.task returns nothing. Which is because task doesn't belong to a user, they belong to owner and assignee. So,
Attempt 2:
class User < ActiveRecord::Base
has_many :tasks, foreign_key: [:owner_id, :assignee_id]
end
class Task < ActiveRecord::Base
belongs_to :user
end
That just fails altogether as two foreign keys don't seem to be supported.
So what I want is to be able to say User.tasks and get both the users owned and assigned tasks.
Basically somehow build a relationship that would equal a query of Task.where(owner_id || assignee_id == 1)
Is that possible?
Update
I'm not looking to use finder_sql, but this issue's unaccepted answer looks to be close to what I want: Rails - Multiple Index Key Association
So this method would look like this,
Attempt 3:
class Task < ActiveRecord::Base
def self.by_person(person)
where("assignee_id => :person_id OR owner_id => :person_id", :person_id => person.id
end
end
class Person < ActiveRecord::Base
def tasks
Task.by_person(self)
end
end
Though I can get it to work in Rails 4, I keep getting the following error:
ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id
TL;DR
class User < ActiveRecord::Base
def tasks
Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
end
end
Remove has_many :tasks in User class.
Using has_many :tasks doesn't make sense at all as we do not have any column named user_id in table tasks.
What I did to solve the issue in my case is:
class User < ActiveRecord::Base
has_many :owned_tasks, class_name: "Task", foreign_key: "owner_id"
has_many :assigned_tasks, class_name: "Task", foreign_key: "assignee_id"
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User"
belongs_to :assignee, class_name: "User"
# Mentioning `foreign_keys` is not necessary in this class, since
# we've already mentioned `belongs_to :owner`, and Rails will anticipate
# foreign_keys automatically. Thanks to #jeffdill2 for mentioning this thing
# in the comment.
end
This way, you can call User.first.assigned_tasks as well as User.first.owned_tasks.
Now, you can define a method called tasks that returns the combination of assigned_tasks and owned_tasks.
That could be a good solution as far the readability goes, but from performance point of view, it wouldn't be that much good as now, in order to get the tasks, two queries will be issued instead of once, and then, the result of those two queries need to be joined as well.
So in order to get the tasks that belong to a user, we would define a custom tasks method in User class in the following way:
def tasks
Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
end
This way, it will fetch all the results in one single query, and we wouldn't have to merge or combine any results.
Extending upon #dre-hh's answer above, which I found no longer works as expected in Rails 5. It appears Rails 5 now includes a default where clause to the effect of WHERE tasks.user_id = ?, which fails as there is no user_id column in this scenario.
I've found it is still possible to get it working with a has_many association, you just need to unscope this additional where clause added by Rails.
class User < ApplicationRecord
has_many :tasks, ->(user) {
unscope(:where).where(owner: user).or(where(assignee: user)
}
end
Rails 5:
you need to unscope the default where clause
see #Dwight answer if you still want a has_many associaiton.
Though User.joins(:tasks) gives me
ArgumentError: The association scope 'tasks' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.
As it is no longer possible you can use #Arslan Ali solution as well.
Rails 4:
class User < ActiveRecord::Base
has_many :tasks, ->(user){ where("tasks.owner_id = :user_id OR tasks.assignee_id = :user_id", user_id: user.id) }
end
Update1:
Regarding #JonathanSimmons comment
Having to pass the user object into the scope on the User model seems like a backwards approach
You don't have to pass the user model to this scope.
The current user instance is passed automatically to this lambda.
Call it like this:
user = User.find(9001)
user.tasks
Update2:
if possible could you expand this answer to explain what's happening? I'd like to understand it better so I can implement something similar. thanks
Calling has_many :tasks on ActiveRecord class will store a lambda function in some class variable and is just a fancy way to generate a tasks method on its object, which will call this lambda. The generated method would look similar to following pseudocode:
class User
def tasks
#define join query
query = self.class.joins('tasks ON ...')
#execute tasks_lambda on the query instance and pass self to the lambda
query.instance_exec(self, self.class.tasks_lambda)
end
end
I worked out a solution for this. I'm open to any pointers on how I can make this better.
class User < ActiveRecord::Base
def tasks
Task.by_person(self.id)
end
end
class Task < ActiveRecord::Base
scope :completed, -> { where(completed: true) }
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
def self.by_person(user_id)
where("owner_id = :person_id OR assignee_id = :person_id", person_id: user_id)
end
end
This basically overrides the has_many association but still returns the ActiveRecord::Relation object I was looking for.
So now I can do something like this:
User.first.tasks.completed and the result is all completed task owned or assigned to the first user.
Since Rails 5 you can also do that which is the ActiveRecord safer way:
def tasks
Task.where(owner: self).or(Task.where(assignee: self))
end
My answer to Associations and (multiple) foreign keys in rails (3.2) : how to describe them in the model, and write up migrations is just for you!
As for your code,here are my modifications
class User < ActiveRecord::Base
has_many :tasks, ->(user) { unscope(where: :user_id).where("owner_id = ? OR assignee_id = ?", user.id, user.id) }, class_name: 'Task'
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
end
Warning:
If you are using RailsAdmin and need to create new record or edit existing record,please don't do what I've suggested.Because this hack will cause problem when you do something like this:
current_user.tasks.build(params)
The reason is that rails will try to use current_user.id to fill task.user_id,only to find that there is nothing like user_id.
So,consider my hack method as an way outside the box,but don't do that.
Better way is using polymorphic association:
task.rb
class Task < ActiveRecord::Base
belongs_to :taskable, polymorphic: true
end
assigned_task.rb
class AssignedTask < Task
end
owned_task.rb
class OwnedTask < Task
end
user.rb
class User < ActiveRecord::Base
has_many :assigned_tasks, as: :taskable, dependent: :destroy
has_many :owned_tasks, as: :taskable, dependent: :destroy
end
In result, we can use it so:
new_user = User.create(...)
AssignedTask.create(taskable: new_user, ...)
OwnedTask.create(taskable: new_user, ...)
pp user.assigned_tasks
pp user.owned_tasks
I need to link Comments to a Post. However the Comment could be (user generated) a simple text, (system generated) a link or an (system generated) image.
At first they all shared the same attributes. So I just needed to create a category attribute, and do different stuff with the text attribute based on that category.
example:
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :author, :class_name => "User"
CATEGORY_POST = "post"
CATEGORY_IMAGE = "image"
CATEGORY_LINK = "link"
validates :text, :author, :category, :post, :presence => true
validates_inclusion_of :category, :in => [CATEGORY_POST, CATEGORY_IMAGE, CATEGORY_LINK]
attr_accessible :author, :text, :category, :post
def is_post?
self.category == CATEGORY_POST
end
def is_link?
self.category == CATEGORY_LINK
end
def is_image?
self.category == CATEGORY_IMAGE
end
end
However this wil not suffice now, because I doesn't feel clean to dump every value in a generic "text" property. So I was thinking about create a polymorphic model (and if needed in a factory pattern). But when I googled about polymorphic models, I get examples like a Comment on a Post, but the same Comment on a Page, kind of relations. Is my understanding of polymorphic different (a model that acts different in different situations, compared to a model that acts the same under different scopes)?
So how would I set up this kind of relationship?
I was thinking of (and please correct me)
Post
id
Comment
id
post_id
category (a enum/string or integer)
type_id (references either PostComment, LinkComment or ImageComment based on category)
author_id
PostComment
id
text
LinkComment
id
link
ImageComment
id
path
User (aka Author)
id
name
But I have no clue how to setup the model so that I can call post.comments (or author.comments) to get all comments. A nice to have would be that the creation of a comment would be through comment and not link/image/postcomment (comment acting as the factory)
My main question is, how to setup up the activerecord models, so the relations stay intact (a author has comments and a post has comments. Comments being either a Link, Image or Postcomment)
I'm going to answer only your main question, the model setup. Given the columns and tables you used in your question, with the exception of Comment, you can use the following setup.
# comment.rb
# change category to category_type
# change type_id to category_id
class Comment < ActiveRecord::Base
belongs_to :category, polymorphic: true
belongs_to :post
belongs_to :author, class_name: 'User'
end
class PostComment < ActiveRecord::Base
has_one :comment, as: :category
end
class LinkComment < ActiveRecord::Base
has_one :comment, as: :category
end
class ImageComment < ActiveRecord::Base
has_one :comment, as: :category
end
with that setup, you can do the following.
>> post = Post.first
>> comments = post.comments
>> comments.each do |comment|
case comment.category_type
when 'ImageComment'
puts comment.category.path
when 'LinkComment'
puts comment.category.link
when 'PostComment'
puts comment.category.text
end
end
A Contact has a User assigned to them:
class Contact < ActiveRecord::Base
...
belongs_to :user
...
end
The user model has a field I want to exclude any time a user object or objects are returned from db. One of the ways to make it work is to add a default scope:
class User < ActiveRecord::Base
...
has_many :contacts
...
default_scope select((column_names - ['encrypted_password']).map { |column_name| "`#{table_name}`.`#{column_name}`"})
end
So in console if I do:
User.first
The select statement and result set do not include 'encrypted_password'.
However, if I do:
c = Contact.includes(:user).first
c.user
they do. The default scope on the User model does not get applied in this case and the 'encrypted_password' field is shown.
So my question is why? And also, is there a clean way to specify what fields should be returned on related object(s)?
You should just be able to use the :select option on the belongs_to relationship. Something like this:
class Contact < ActiveRecord::Base
...
belongs_to :user, :select => [:id, :first_name, :last_name, :email]
...
end
I have the usual polymorphic associations for comments:
class Book < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Article < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
I'd like to be able to define Book.recently_commented, and Article.recently_commented based on the created_at timestamp on the comments. Right now I'm looking at a pretty ugly find_by_SQL query to do this with nested selects. It seems as though there must be a better way to do it in Rails without resorting to SQL.
Any ideas? Thanks.
For what it's worth, here's the SQL:
select * from
(select books.*,comments.created_at as comment_date
from books inner join comments on books.id = comments.commentable_id
where comments.commentable_type='Book' order by comment_date desc) as p
group by id order by null;
Sometimes it's just best to add a field to the object of which you are commenting. Like maybe a commented_at field of datetime type. When a comment is made on an object, simply update that value.
While it is possible to use SQL to do it, The commented_at method may prove to be much more scalable.
Not sure what your method has looked like previously but I'd start with:
class Book < ActiveRecord::Base
def self.recently_commented
self.find(:all,
:include => :comments,
:conditions => ['comments.created_at > ?', 5.minutes.ago])
end
end
This should find all the books that have had a comment created on them in the last 5 minutes. (You might want to add a limit too).
I'd also be tempted to create a base class for this functionality to avoid repeating the code:
class Commentable < ActiveRecord::Base
self.abstract_class = true
has_many :comments, :as => :commentable
def self.recently_commented
self.find(:all,
:include => :comments,
:conditions => ['comments.created_at > ?', Time.now - 5.minutes])
end
end
class Book < Commentable
end
class Article < Commentable
end
Also, you might want to look at using a plugin to achieve this. E.g. acts_as_commentable.