Maintain order of saved attributes list for model - ruby-on-rails

I have two models with a has_many to has_many relationship through a join table.
class Article < ActiveRecord::Base
has_many :authorings, -> { order(:position) }, :dependent => :destroy
has_many :authors, through: :authorings
end
class Author < ActiveRecord::Base
has_many :authorings, -> { order(:position) }
has_many :articles, through: :authorings
end
class Authoring < ActiveRecord::Base
belongs_to :author
belongs_to :article
acts_as_list :scope => :author
end
The getter and setter methods for the array
def author_list
self.authors.collect do |author|
author.name
end.join(', ')
end
def author_list=(author_array)
author_names = author_array.collect { |i|
i.strip.split.each do |j|
j.capitalize
end.join(' ') }.uniq
new_or_found_authors = author_names.collect { |name| Author.find_or_create_by(name: name) }
self.authors = new_or_found_authors
end
I want to maintain the order of the list of authors that get saved to the model. That is, I would like to be able to change and reorder the author_list and have it retrieved in that order for the view. I want to be to change it ['foo','bar'] or ['bar','foo']. How can I do this?
As a note, I have tried using acts_as_list and added a position column to the database for authoring but to no success.

You need to have a position attribute for each author.
Then you can generate a sorted activerecord array and grab the name attribute
authorlist = Author.order(:position).pluck(:name)
I'm not seeing how you're changing the position attribute, I'm guessing you need some sort of js on the frontend to do that.

Related

Avoiding N+1 queries in Ruby on Rails with nested associations and conditions

I'm working on a podcast player and wish to display a list of recently updated feeds, along with details of the play time remaining for the most recently published entry.
So the view looks something like:
#feeds.each do |f|
puts #feed.rss_image.url
puts #feed.most_recent_entry.published_at
if play = #feed.most_recent_entry.most_recent_play_by(#user)
puts play.remaining
end
end
My models are as follows:
class Feed < ApplicationRecord
has_one :rss_image, as: :rss_imageable
has_many :entries, dependent: :destroy
has_one :most_recent_entry, -> { order(published_at: :desc) }, class_name: "Entry"
has_many :plays, dependent: :destroy
end
class Entry < ApplicationRecord
belongs_to :feed, touch: true
has_many :plays, dependent: :destroy
has_one :most_recent_play, -> { order(updated_at: :desc) }, class_name: "Play"
def most_recent_play_by(user)
plays.by(user).order(updated_at: :desc).first
end
end
class Play < ApplicationRecord
belongs_to :entry
belongs_to :feed
belongs_to :user
scope :by, ->(user) { where(user: user) }
def self.most_recent_by(user)
by(user).order(updated_at: :desc).first
end
end
My query is:
#feeds = Feed
.joins(:entries)
.includes(:rss_image, most_recent_entry: :most_recent_play)
.where(most_recent_entry: {plays: {user: #user}})
.group(:id)
.order("max(entries.published_at) DESC")
.limit(10)
But this errors with:
PG::GroupingError: ERROR: column "rss_images.id" must appear in the GROUP BY clause or be used in an aggregate function
Is it possible to achieve this without N+1 queries?
Thanks!
Take a look to Bullet gem, it helps to reduce the number of queries and eliminate n+1. In this case it should suggest you how to modify you query, eg. adding .includes(:entries) ....

How to create a new association in Rails which eager loads other associations?

So I have this sort of system currently:
class Tag < ActiveRecord::Base
# attr :name
end
class Article < ActiveRecord::Base
has_many :tag_associations, dependent: :destroy
has_many :tags, through: :tag_associations
end
class Company < ActiveRecord::Base
has_many :articles
end
class User < ActiveRecord::Base
belongs_to :company
end
I would like to do this:
user.company.articles.includes(:tags).all
I am going to use it like this (I already have a question on the eager-loading aspect of this):
company.articles.all.select do |article|
article.tags.any? do |tag|
tag.name == 'foo'
end
end
How can I make a new association on the company object for articles_by_tag?
class Company < ActiveRecord::Base
has_many :articles
has_many :articles_by_tag, scope -> { |tag| ... }
end
And how would I end up using it?
company.articles_by_tag(tag: 'foo').all
This is a textbook X & Y question. Eager loading everything and then using .select with a block to loop through it in Ruby is basically like ordering every pizza at a restaurant just to find the one that has anchovies on it and tossing everything else.
Instead you use a INNER JOIN to filter the rows:
Acticle.joins(:tags)
.where(tags: { name: 'foo' })
This will only return rows with a match in the tags table.
If you want to write this into a scope you would do:
class Article < ApplicationRecord
scope :with_tag, ->(name){ joins(:tags).where(tags: { name: name }) }
end
As a bonus you can use GROUP and HAVING to find articles with a list of tags:
class Acticle < ApplicationRecord
def self.with_tags(*tags)
left_joins(:tags)
.group(:id)
.where(tags: { name: tag })
.having(Tag.arel_table[Arel.star].count.eq(tags.length))
end
end

Sorting as association before saving using callbacks

I recently migrated from Rails 3 to Rails 4 and in the process I noticed that sorting association does not work in Rails 4. Following are the sample models:
#box.rb
class Box < ActiveRecord::Base
has_many :items
accepts_nested_attributes_for :items, :allow_destroy => true
before_validate
items.sort! { <some condition> }
end
end
#item.rb
class Item < ActiveRecord::Base
belongs_to :box
end
In Rails 3, sort! method on the association modified the items hash, but in Rails 4 it returns a new sorted instance but does not modify the actual instance. Is there a way to overcome this?
Try this:
#box.rb
class Box < ActiveRecord::Base
has_many :items
accepts_nested_attributes_for :items, :allow_destroy => true
before_validate
self.items = items.sort { <some condition> }
end
end
#item.rb
class Item < ActiveRecord::Base
belongs_to :box
end
Sorting before storing won't really help. When you pull them from the database they might not be in that order. You wan't to specify the order on the has many (or in the item model).
If you have a more complicated ordering, then please post that logic.
class Box < ActiveRecord::Base
# Can replace position with hash like: { name: :asc }
has_many :item, -> { order(:position) }
accepts_nested_attributes_for :items, :allow_destroy => true
before_validate
items.sort! { <some condition> }
end
end

Rails: Querying through 2 join table associations

I have a quite complicated relation between models and are now frustrated by a SQL Query to retrieve some objects.
given a Product model connected to a category model via a has_many :through association and a joint table categorization.
Also a User model connected to this category model via a has_many :through association and a joint table *category_friendship*.
I am now facing the problem to retrieve all products, which are within the categories of the array user.category_ids. However, I can't just not manage to write the WHERE statement properly.
I tried this:
u = User.first
uc = u.category_ids
Product.where("category_id IN (?)", uc)
However this won't work, as it doesn't have a category_id in the product table directly. But how can I change this to use the joint table categorizations?
I'm giving you the model details, maybe you find it helpful for answering my question:
Product.rb
class Product < ActiveRecord::Base
belongs_to :category
def self.from_users_or_categories_followed_by(user)
cf = user.category_ids
uf = user.friend_ids
where("user_id IN (?)", uf) # Products out of friend_ids (uf) works fine, but how to extend to categories (cf) with an OR clause?
end
Category.rb
class Category < ActiveRecord::Base
has_many :categorizations
has_many :products, through: :categorizations
has_many :category_friendships
has_many :users, through: :category_friendships
Categorization.rb
class Categorization < ActiveRecord::Base
belongs_to :category
belongs_to :product
Category_friendship.rb
class CategoryFriendship < ActiveRecord::Base
belongs_to :user
belongs_to :category
User.rb
class User < ActiveRecord::Base
has_many :category_friendships
has_many :categories, through: :category_friendships
def feed
Product.from_users_or_categories_followed_by(self) #this should aggregate the Products
end
If you need more details to answer, please feel free to ask!
Looking at the associations you have defined and simplifying things. Doing a bit refactoring in what we have to achieve.
Product.rb
class Product < ActiveRecord::Base
belongs_to :category
end
User.rb
class User < ActiveRecord::Base
has_many :categories, through: :category_friendships
scope :all_data , includes(:categories => [:products])
def get_categories
categories
end
def feed
all_products = Array.new
get_categories.collect {|category| category.get_products }.uniq
end
end
Category.rb
class Category < ActiveRecord::Base
has_many :users, through: :category_friendships
has_many :products
def get_products
products
end
end
NO NEED OF CREATING CATEGORY_FRIENDSHIP MODEL ONLY A JOIN TABLE IS NEEDED WITH NAME CATEGORIES_FRIENSHIPS WHICH WILL JUST HAVE USER_ID AND CATEGORY_ID
USAGE: UPDATED
Controller
class UserController < ApplicationController
def index
#all_user_data = User.all_data
end
end
view index.html.erb
<% for user in #all_user_data %>
<% for products in user.feed %>
<% for product in products %>
<%= product.name %>
end
end
end
I've upvoted Ankits answer but I realized there is a more elegant way of handeling this:
given:
u = User.first
uc = u.category_ids
then I can retrieve the products out of the categories by using:
products = Product.joins(:categories).where('category_id IN (?)', uc)

Rails order by in associated model

I have two models in a has_many relationship such that Log has_many Items. Rails then nicely sets up things like: some_log.items which returns all of the associated items to some_log. If I wanted to order these items based on a different field in the Items model is there a way to do this through a similar construct, or does one have to break down into something like:
Item.find_by_log_id(:all,some_log.id => "some_col DESC")
There are multiple ways to do this:
If you want all calls to that association to be ordered that way, you can specify the ordering when you create the association, as follows:
class Log < ActiveRecord::Base
has_many :items, :order => "some_col DESC"
end
You could also do this with a named_scope, which would allow that ordering to be easily specified any time Item is accessed:
class Item < ActiveRecord::Base
named_scope :ordered, :order => "some_col DESC"
end
class Log < ActiveRecord::Base
has_many :items
end
log.items # uses the default ordering
log.items.ordered # uses the "some_col DESC" ordering
If you always want the items to be ordered in the same way by default, you can use the (new in Rails 2.3) default_scope method, as follows:
class Item < ActiveRecord::Base
default_scope :order => "some_col DESC"
end
rails 4.2.20 syntax requires calling with a block:
class Item < ActiveRecord::Base
default_scope { order('some_col DESC') }
end
This can also be written with an alternate syntax:
default_scope { order(some_col: :desc) }
Either of these should work:
Item.all(:conditions => {:log_id => some_log.id}, :order => "some_col DESC")
some_log.items.all(:order => "some_col DESC")
set default_scope in your model class
class Item < ActiveRecord::Base
default_scope :order => "some_col DESC"
end
This will work
order by direct relationship has_many :model
is answered here by Aaron
order by joined relationship has_many :modelable, through: :model
class Tournament
has_many :games # this is a join table
has_many :teams, through: :games
# order by :name, assuming team has this column
def teams
super.order(:name)
end
end
Tournament.first.teams # are returned ordered by name
For anyone coming across this question using more recent versions of Rails, the second argument to has_many has been an optional scope since Rails 4.0.2. Examples from the docs (see scopes and options examples) include:
has_many :comments, -> { where(author_id: 1) }
has_many :employees, -> { joins(:address) }
has_many :posts, ->(blog) { where("max_post_length > ?", blog.max_post_length) }
has_many :comments, -> { order("posted_on") }
has_many :comments, -> { includes(:author) }
has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
has_many :tracks, -> { order("position") }, dependent: :destroy
As previously answered, you can also pass a block to has_many. "This is useful for adding new finders, creators and other factory-type methods to be used as part of the association." (same reference - see Extensions).
The example given there is:
has_many :employees do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by(first_name: first_name, last_name: last_name)
end
end
In more modern Rails versions the OP's example could be written:
class Log < ApplicationRecord
has_many :items, -> { order(some_col: :desc) }
end
Keep in mind this has all the downsides of default scopes so you may prefer to add this as a separate method:
class Log < ApplicationRecord
has_many :items
def reverse_chronological_items
self.items.order(date: :desc)
end
end

Resources