Rails after_find callback ignore query - ruby-on-rails

So, I call method Article.getByTag().
Callback after_find work, but ignore query (in which get all tags by current article).
It's rails log
Started GET "/article/lorem" for 127.0.0.1 at 2014-01-22 08:51:11 +0400
Processing by ArticleController#show_tag as HTML
Parameters: {"tagname"=>"lorem"}
Tags Load (0.2ms) SELECT `tags`.* FROM `tags` WHERE `tags`.`name` = 'lorem' LIMIT 1
Article Load (0.1ms) SELECT `articles`.* FROM `articles` INNER JOIN `tag2articles` ON `tag2articles`.`article_id` = `articles`.`id` WHERE `tag2articles`.`Tags_id` = 5
start set article tags for article 3
getTags by article 3
end set article tags
Article class:
class Article < ActiveRecord::Base
attr_accessible :text, :title
attr_accessor :tags
has_many :tag2articles
after_find do |article|
logger.info 'start set article tags for article '+article.id.to_s
article.tags = Tag2article.getTags(article.id)
logger.info 'end set article tags'
end
def self.getByTag(tagname)
#tag = Tags.get(tagname)
Article.joins(:tag2articles).where('tag2articles.Tags_id' => #tag[:id]).all()
end
end
and class Tag2article
class Tag2article < ActiveRecord::Base
belongs_to :Article
belongs_to :Tags
attr_accessible :Article, :Tags
def self.getTags(article)
logger.info 'getTags by article '+article.to_s
#tags = Tag2article.joins(:Tags).where(:Article_id => article).select('tags.*')
return #tags
end
def self.getArticle(tag)
Tag2article.where(:Tags_id => tag).joins(:Article).select('articles.*')
end
end

Basically, this is all you need:
Article class:
class Article < ActiveRecord::Base
has_many :tags_to_articles
has_many :tags, through: :tags_to_articles
end
and the model class for the join table
class TagsToArticle < ActiveRecord::Base
belongs_to :article
belongs_to :tag
end
and you fetch all articles by tag name by doing:
Article.joins(:tags).where('tags.name' => tag_name)
I'm writing this from my head and I have NO idea what you managed to implement in your code so far, so please excuse me if the code doesn't work from the start - play with it and make it work.
Also, I suggest starting a new tutorial project, you reinvented so many wheels there (notice that you don't need an after_find hook because has_many :tags, through: :tags_to_articles does this for you) and did so many funny stuff (like naming attributes of a class the same name as the class it references) that you'll just burn yourself in frustration.
Also, if you use rails 4 you don't need to do attr_accessible dance, and if you don't I suggest taking a look at strong_parameters gem https://github.com/rails/strong_parameters
See: http://guides.rubyonrails.org/association_basics.html
AND TAKE SPECIAL NOTE OF NAMING CONVENTIONS - singular and plural, camelcase and hungarian notation, uppercase and lowercase, they all have their place and their meaning and the tutorials usually make a good job of taking you through it
Ruby and Rails are quite beautiful and powerful, but only if you let them. There's no benefit inherent in the language that will somehow infuse your code if you're still essentially writing Java.

Related

ActiveRecord query to join Posts, post authors and likes in Rails 4

So, I have read through quite a few rails active records pages, stack O questions and answers (about 12 hours of time) trying to figure out how the heck to tie all of these things together into a single query to display them on my page.
Here is my page view
Secrets with owner info
</h3>
<% #secretInfo.each do |i| %>
<p><%= i.content %> - <%= i.first_name %></p>
<p><%= i.created_at %></p>
--> "this is where I'd like to have likes for post" <--
<% end %>
and here is my controller
def show
#user = User.find(params[:id])
#secrets = Gossip.all
#mySecrets = Gossip.where(user_id: [params[:id]])
#secretInfo = Gossip.joins(:user).select("content", "first_name", "created_at")
#secretWLikesNInfo = WTF MATE?
end
Also, may help to see my models and schema so here are those
class User < ActiveRecord::Base
attr_accessor :password
has_many :gossips
has_many :likes
has_many :liked_secrets, :through => :gossips, :source => :gossip
class Like < ActiveRecord::Base
belongs_to :user
belongs_to :gossip
class Gossip < ActiveRecord::Base
belongs_to :user
has_many :likes
has_many :liking_users, :through => :likes, :source => :user
I don't know why this seems so impossible or it could be something very simple that I am just overlooking. This all was very easy in PHP/MySQL. All help is appreciated.
Additional points for coming up with a query that allows me to see all posts that I as a user has created AND liked!
Well, what you want to do is eager loading: load data associated with a record in a single roundtrip to the database. For example, i think you can load all your data like this:
#user = User.where(id: params[:id])
.joins(:liked_secrets)
.includes(:liked_secrets => :likes)
.first!
#secretInfo = #user.liked_secrets.map do |secret|
OpenStruct.new(
content: secret.content,
first_name: user.first_name,
created_at: secret.created_at,
likes: secret.likes
)
end
This works by including in the data fetched from the database in the first query all the data associated included in the include parameter. So, calling #user.liked_secrets will return the secrets but won't call the database because that information already came from the database in the first query. The same happens if you do #user.liked_secrets.first.likes because of the :linked_secrets => :likes parameter on the initial query.
I'll let a link to a good blog post about this here:
http://blog.arkency.com/2013/12/rails4-preloading/.
And, if you feel the Rails ORM (ActiveRecord) doesn't really works for your use case, you can just use sql in a string or fallback to use another Ruby ORM out there (like Sequel).

Rails - how to join/group nested tables and get joined values out?

I have the following hierarchy of models where each one has_many of the one below it:
class AccountGroup < ActiveRecord::Base
has_many :accounts, :inverse_of=>:account_group
# name: string
class Account < ActiveRecord::Base
belongs_to :accountGroup, :inverse_of=>:account
has_many :positions, :inverse_of=>:account
class Position < ActiveRecord::Base
belongs_to :account, :inverse_of=>:positions
# net_position: integer
In other words, an AccountGroup contains a bunch of Accounts, and an Account contains a bunch of Positions.
Goal: I want an hash of AccountGroup => (sum of its net_positions). That means there's a GROUP BY involved.
I can do this with raw SQL, but I haven't cracked it with Rails functions. The raw SQL is:
SELECT account_groups.id,SUM(net_position),account_groups.name
FROM account_groups
LEFT JOIN accounts ON accounts.account_group_id = account_groups.id
LEFT JOIN positions ON positions.account_id = accounts.id
GROUP BY account_groups.id,account_groups.name;
Is this something that Rails just can't do?
Rails (4.0.0) can do this - we have two ways to do it currently:
1. SQL "Alias" Columns
Rails Scoping For has_many :through To Access Extra Data
#Images
has_many :image_messages, :class_name => 'ImageMessage'
has_many :images, -> { select("#{Image.table_name}.*, #{ImageMessage.table_name}.caption AS caption") }, :class_name => 'Image', :through => :image_messages, dependent: :destroy
2. ActiveRecord Association Extensions
This is a little-known feature of Rails, which allows you to play with the collection object. The way it does it is to extend the has_many relationship you have created:
class AccountGroup < ActiveRecord::Base
has_many :accounts do
def X
#your code here
end
end
end
We have only got this method working for collections, but you can do all sorts with it. You should look at this tutorial to see more about it
Update
We just got this working by using an extension module:
#app/models/message.rb
Class Message < ActiveRecord::Base
has_many :image_messages #-> join model
has_many :images, through: :image_messages, extend: ImageCaption
end
#app/models/concerns/image_caption.rb
module ImageCaption
#Load
def load
captions.each do |caption|
proxy_association.target << caption
end
end
#Private
private
#Captions
def captions
return_array = []
through_collection.each_with_index do |through,i|
associate = through.send(reflection_name)
associate.assign_attributes({caption: items[i]})
return_array.concat Array.new(1).fill( associate )
end
return return_array
end
#######################
# Variables #
#######################
#Association
def reflection_name
proxy_association.source_reflection.name
end
#Foreign Key
def through_source_key
proxy_association.reflection.source_reflection.foreign_key
end
#Primary Key
def through_primary_key
proxy_association.reflection.through_reflection.active_record_primary_key
end
#Through Name
def through_name
proxy_association.reflection.through_reflection.name
end
#Through
def through_collection
proxy_association.owner.send through_name
end
#Captions
def items
through_collection.map(&:caption)
end
#Target
def target_collection
#load_target
proxy_association.target
end
end
Props to this gist for the variable functions
This basically overrides the load ActiveRecord function in the CollectionProxy class, and uses it to create our own proxy_association.target array :)
If you need any information on how to implement, just ask in the comments
You can make this little bit more prettier than raw sql by using rails AR querying methods:
AccountGroup.
select("account_groups.id, SUM(net_position), account_groups.name").
joins("LEFT JOIN accounts ON accounts.account_group_id = account_groups.id").
joins("LEFT JOIN positions ON positions.account_id = accounts.id").
group("account_groups.id,account_groups.name")
This can be done with pure Arel as well.
AccountGroup.select(
AccountGroup.arel_table[:id], Arel::Nodes::NamedFunction.new('SUM', [:net_position]), AccountGroup.arel_table[:name]
).joins(
AccountGroup.arel_table.join(Account.arel_table).on(
Account.arel_table[:account_group_id].eq(AccountGroup.arel_table[:id])
).join_sources
).joins(
AccountGroup.arel_table.join(Position.arel_table).on(
Position.arel_table[:account_id].eq(Account.arel_table[:id])
).join_sources
).group(
AccountGroup.arel_table[:id], AccountGroup.arel_table[:name]
)
I'm not 100% sure this will work, I simply copied your SQL from above and put it into scuttle.io
Use include function, in example
ac = AccountGroup.all(:include => :account)
$ AccountGroup Load (0.6ms) SELECT `account_groups`.* FROM `groups`
$ Account Load (16.4ms) SELECT `accounts`.* FROM `accounts` WHERE `accounts`.`id` IN (1010, 3, 4, 202, 203, 204, 9999)
Then you can call ac.account.name or something like that
There are a great Railscast http://railscasts.com/episodes/22-eager-loading?view=asciicast
If you really want to use ActiveRecord for this (no SQL), it will be something like:
ags = AccountGroup.all(:include => {:accounts => :positions})
hash = Hash[ags.map { |ag| [ag, ag.map(&:accounts).flatten.map(&:positions).flatten.map(&:net_position).reduce(0,&:+)]}]
But it will be slower than your SQL, and not any prettier.
Is this something that Rails just can't do?
As this question has been open for about a month, I'm gonna to go ahead and assume the answer to this question is...
Yes.
EDIT: Yes, for Rails 3. But Rails 4 can do it! See accepted answer.
Rails can't do it, outside of using find_by_sql or ActiveRecord::Base.connection.execute(query), which are pretty kludgy and not rails-y.

How do you query upstream in a polymorphic association without raw SQL?

In my exercise app, users can "check" an exercise to complete it. If I know that checkable_type is 'Exercise' and have the checkable_id, how do I query the exercise name?
I'm not sure what to put in my controller to have a list of "completed exercises" and their names based on checkable_type and id.
class Exercise < ActiveRecord::Base
attr_accessible :name
has_many :checks, :as => :checkable
end
class Check < ActiveRecord::Base
belongs_to :checkable, :polymorphic => true
attr_accessible :checkable, :checkable_id, :checkable_type
end
Would just joining the other model give you what you wanted?
completed_exercises = Exercise.joins(:checks)
That should produce an inner join, and AR already knows how to include the polymorphic association as part of the query.
You can define a scope checkable_type
scope :checkable_type, lambda { |class_name| where("checkable_type = ?", class_name) }
I guess you want the polymorphic records with type 'Exercise' and that would be created only when exercises are completed or you can maintain it in a completed_at timestamp column for that matter.
Then you can pulls a list of all completed exercises
scope :completed, where('completed_at IS NOT NULL')
completed_exercises = Check.checkable_type('Exercise').completed
You may ignore the completed scope if that's not the case
Figured it out!
class Exercise < ActiveRecord::Base
attr_accessible :exercise_name
has_many :checks, as: :checkable
end
class Check < ActiveRecord::Base
belongs_to :checkable, polymorphic: true
attr_accessible :checkable_id, :checkable_type, :exercise_name
end
I realized I needed to call .checkable before calling the desired attributes in the exercise model.
<% #checks.each do |check| %>
<%= check.checkable.exercise_name %>
<%= check.checkable.created_at %>
<% end %>
This pulls the right exercise name and "completion time" (via created_at) from the exercise table.

Retrieving model attribute from table+column name

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.

Rails: Query to get recent items based on the timestamp of a polymorphic association

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.

Resources