Order Model by the date difference of its has_many model - ruby-on-rails

I don't know this is the exact and simple way I am implementing. I have a two model. Modal 'A' belongs to Modal 'B' and 'B' has many to Modal 'A'. I have a date field 'expiry date' in Model B.
Now I want to list out records the Modal 'A'. The list should be in ascending order with the difference of its associated Modal 'B' last record expiry date and today date. I calculated the date difference in view, but still confused to order it.
diff = (f.bill_histories.last.ndate.to_date - DateTime.now.to_date).to_i
I tried and google it, but still didn't find suggestion. Please suggest me.

assuming:
class Foo < ActiveRecord::Base
belongs_to :bar
end
class Bar < ActiveRecord::Base
has_many :foos
end
You should be able to sort on the database level using a join statement on the Bars table:
Foo.joins(:bar).order(bars: { expired_at: :desc })
Explanation:
ascending order with the difference of its associated Modal 'B' last record expiry date and today date
This means that the newest date should be first, so a simple ORDER BY expired_at DESC should do what you need.
The join is necessary so you can use the Bars expired_at column.
The hash syntax in the order method is needed to access the join table's attribute.

Checking the difference Time.zone.now - model_b_object.expiry_date is an unnecessary step. Just go directly to the ordering part. This will show the model B records that expired nearest to Today's date.
ModelB.all.order("expiry_date ASC")

Related

Rails 4 ActiveRecord group_by and nested loops

The project I am working on has the following models:
Button: has many extra_prices (one for each Currency)
ExtraPrice: belongs to a product (in that case a Button) and has as an attribute a currency_id
Currency: there is a reference to a currency_id on an ExtraPrice as I mentioned above. FYI there are 4 currencies so far in the app.
Some of the buttons don't have an extra_price set in one of the currencies which causes an error in another part of the app.
I am trying to write a rake task that would:
- check all buttons missing an extra_price for one a more currencies
- find out which currency is missing
- create the extra price
So far I toyed with a few options but I am stuck (I am pretty junior as a dev, and especially on the back-end side/DB query).
I was thinking something like:
Button.transaction do
currencies = Currency.all.pluck :id
buttons_no_extra_price = Button.select { |button| button.extra_prices.length <
currencies.length }
end
and then I'm stuck :)
I would like to do something like
buttons_no_extra_price.group_by(|button| button.extra_prices.currency_ids)
(wrong formatting of course since extra_prices is an array and currency_id is an attribute on each extra_price)
but instead of grouping them by currency_id, I would like to group them by the missing currency_id or ids, maybe using the currencies variable above.
missing_prices = {currency1: [button1, button2], currency2: [button192, button208], currency3: [button392, button220]...}
This way I could loop through every Currency and create an extra_price on each button object of the nested array like:
missing_prices.each |currency, array_of_buttons| do
array_of_buttons.each do |button|
ExtraPrice.create!(currency: currency, product: button)
end
end
I am also thinking that from a performance standpoint it needs to be optimized so maybe work more with includes, joins, etc. but it's a bit above my current abilities to be totally honest.
So any help would be appreciated :)
Thanks!
So I think I follow your question, and if I am this should do the trick. Let me know if you have any questions. Note that there is probably a more performant way to do this, but given that it's a rake task performance won't need to be fully optimized unless you are dealing with millions of records.
all_currency_ids = Currency.all.pluck(:id)
Button.eager_load(:extra_price).group('buttons.id').having('count(extra_prices.id) < ?', all_currency_ids.count).each do |button|
missing_currency_ids = all_currency_ids - button.extra_prices.pluck(:currency_id)
missing_currency_ids.each do |missing_currency_id|
ExtraPrice.create!(currency: Currency.find_by(id: missing_currency_id), product: button)
end
end
Button.eager_load(:extra_price).group('buttons.id').having('count(extra_prices.id) < ?', all_currency_ids.count) is what gets you the buttons with missing extra prices. This hinges on the fact that each button has an extra price per currency, so I hope I interpreted that correctly.
(Note: I'm not 100% familiar with Rails 4, only 5 and 6. The underlying SQL principles remain the same regardless and are adaptable.)
You can do this in a single query if your relationships are set up correctly.
class Button < ApplicationRecord
has_many :extra_prices
has_many :currencies, through: :extra_prices
end
class ExtraPrice < ApplicationRecord
belongs_to :button
belongs_to :currency
end
class Currency < ApplicationRecord
has_many :extra_prices
end
I've added a relationship between Currency and ExtraPrice. This allows us to set up a relationship between Button and Currency using has_many :currencies, through: :extra_prices. Then we can get a Button's Currencies with button.currencies.
Now we can do a left join between Button and ExtraPrice and Currency. A left join, as opposed to the normal inner join, will pick up Buttons that have no ExtraPrices nor Currencies. We can't use Rails's left_joins, it will not do the right thing.
buttons_missing_currencies = Button
.includes(:currencies)
.joins("left join extra_prices ep on ep.button_id = buttons.id")
.joins("left join currencies c on c.id = ep.currency_id")
.having("count(c.id) < ?", Currency.count)
.group("buttons.id")
That will give you all the Buttons which lack a Currency in a single, efficient query. includes(:currencies) means each Button's Currencies will already be loaded avoiding making N+1 queries.
Now we can look through each button, discover which currencies are missing, and fill them in.
all_currencies = Currency.all
buttons_missing_currencies.each do |button|
missing_currencies = all_currencies - button.currencies
missing_currencies.each do |missing_currency|
button.extra_prices.create!(currency: missing_currency)
end
end

Rails Model Next/Prev Navigation Scope with datetime field

I have a model lets say Post which has a column called published_at (datetime). My model has two instance methods called next and prev.
I'm using mysql as a database adapter.
def next
self.class.unscope(:order).online
.where('published_at > ?', published_at)
.order('published_at ASC').first
end
def prev
self.class.unscope(:order).online
.where('published_at < ?', published_at)
.order('published_at DESC').first
end
With the code above the navigation works if every post has another date. If some posts are on the same date just one record of those gets shown.
Goal: Get the next or previous record ordered by published_at datetime column.
Any ideas?
Neither "next" , nor "previous" make sense in SQL. You have to explicitly define sort order by uniq key.
If you have non-uniq column as sort base, you have to add uniq one, id for example.
Then you have to change WHERE clause to accept rows wich have same value for your non-uniq column:
def next
condition = <<~SQL
(published_at = :published_at AND id > :id) OR
(published_at > :published_at)
SQL
self.class.unscope(:order).
online.
where(condition, published_at: published_at, id: id).
order(published_at: :asc, id: :asc).
take
end
prev is constructed similarly.

What is the best way to order by parent and child's created_at?

Suppose I have a parent Comment that has many Replies.
class Comment < ActiveRecord::Base
has_many :replies
class Reply < ActiveRecord::Base
belongs_to :topic
I would like to order my Comments by created_at, but if they have a newer Reply, I would like that parent Comment to be sorted by its most latest Reply's created_at and not its own created_at.
So if for example I have 3 Comments, one posted for Day 1, Day 2, and Day 3. Day 1 being the oldest, but Day 2 has a reply that was posted today, how can I sort it so that the result would be :
[ Day 2, Day 3, Day 1 ]
Where Day 2 has had the the most recent activity, more recent than the Day 3 comment that would have been posted earlier in the day.
What would be the approach for that?
Handling the dates is best done through your models:
class Comment < ActiveRecord::Base
has_many :replies
end
class Reply < ActiveRecord::Base
belongs_to :comment, touch: true
end
Note the touch: true on the belongs_to: comment association. This will update the updated_at attribute of the parent Comment when saving a Reply
Now just order by updated_at when selecting your comments.
The best query for that (as in the one that will perform best) is to add an extra last_activity column. Maintain the latter (automatically, using triggers or some RoR built-in sugar) and add an index on it. You'll then be able to fetch rows ordered by that column using trivial queries.
The alternative option is an ugly join with an aggregate (see the answer from an hour ago). It won't be pretty and it will perform terribly as your table grows in size.
I imagine something like this might be "okay", but check out the query plan. (I only deal with LINQ and SQL Server myself - YMMV.)
select * from comments c
left join (select r.comment_id, max(r.created_at) as created_at
from replies r
group by r.comment_id) lr
on lr.comment_id = c.comment_id
order by isnull(lr.created_at, c.created_at) desc
After a LEFT JOIN to the latest response (which may not exist, hence the LEFT), use (SQL standard) COALESCE in ORDER BY. But only SELECT columns from comments, according to your requirement:
SELECT c.*
FROM comments c
LEFT JOIN (
SELECT comment_id, max(created_at) AS created_at
FROM replies
GROUP BY 1
) r USING (comment_id)
ORDER BY COALESCE(r.created_at, c.created_at) DESC
Since logic dictates that replies must come after comments. If that wasn't so, you'd use GREATEST(r.created_at, c.created_at) instead.

See if one person is before another in the alphabet, ruby, rails

I'm doing an app for a membership database.
Each person may have a partner. When it comes to displaying the list, I only want to have one row for each family, so at the moment I'm comparing first names and not displaying the row if the person's name is second. Like this
person.first_name != [person.first_name, person.partner.first_name].sort[0]
This means each family only gets displayed once, not twice - once for each partner.
And I'm doing this in the view.
There must be a better way of doing this, and it'd be really great if I could do it at the database level. I'm using postgresql if that makes a difference.
Edit
Sorry if it was unclear.
Say Person 1 has the first_name "Edward" and Person 2 has the first_name "Fay". Edward and Fay are married.
I only want to show them once in my list - I want a row to look like this
Surname First name Address etc
Mysurname Edward ....
Fay
I don't want to display it again with Fay first because I've got both Fay and Edward in list of people, so I use the ruby in the first part of the question to check if I should display the row - it compares their first names and only does the row if the person has a fist name that's before his/her partner's first name.
Here's the relevant part of my person model
class Person < ActiveRecord::Base
has_one :relationship_link, :foreign_key => :person_id, :dependent => :destroy, :include => :partner
has_one :partner, :through => :relationship_link, :source => :person_b, :class_name => "Person"
I hope that's clearer
You need to use DISTINCT ON or GROUP BY. In postgres you need to be careful to group by everything that you are selecting. If you only need to get the last names you can select("DISTINCT ON(last_name) last_name").pluck("last_name"). You will only get an array of last names though.
Maybe you can get records if you order by every other fields in your table, like this:
select("DISTINCT ON(people.last_name) people.*").order("people.last_name ASC, people.first_name ASC, people.field2 DESC, people.field3 ASC...")
You need to order by every attribute so the result is not ambigious.
For this case, i would create a data structure (a Hash) to store people instances given a specific surname. Something like this:
def build_surnames_hash(people_array)
surnames_hash = {}
people_array.each do |person|
last_name = person.last_name
surnames_hash[last_name] ||= []
surnames_hash[last_name] << person
end
surnames_hash
end
That way, you can iterate over the hash and display people using their surnames stored as hash's keys:
surnames_hash = build_surnames_hash(Person.all)
surnames_hash.each do |surname, person_instances_array|
# display the surname once
# iterate over person_instances_array displaying their properties
end

Order table by grouping values in another table

I have a table of questions: title
Each question has answers, in another table: text, question_id
Each answer has votes, in another table: answer_id
I can ask for total votes with
question.votes.count
I have seen how to group the votes db at
http://guides.rubyonrails.org/active_record_querying.html#group
Vote.group("answer_id")
But I want to order my questions by votes. That is ordering an array from a table via the grouping of another table. Is that possible?
q=Question.last
q.answers.order_by_vote #How to do this?
I think my answer could be doing a scope on the answer model that makes the grouping on the subset of votes that belong to the question. Am I right?
Thank you.
First, I think that you mean table when you say DB (database). A table a structure inside a database that holds data for a specific model, for example (questions, votes and answers. In your case you have three tables.
Second, calling attr_accessible :answer_id does not make an attribute searchable by an index. It is searchable by this attribute regardless of whether or not you declare it accessible. It is not an index unless you specify it as an index in the database. attr_accessible means that the attribute can be set using mass-assignment, for example when you call update_attributes.
Finally, if you want to group by answer_id and order by the number of votes, you can do so with the following query:
Vote.select('count(id) as votes, answer_id').group('answer_id').order('votes DESC')
If you want to order by some other value than the one you are grouping by, you'll have to add that to your group as well. This ensures that you aren't getting arbitrary results for the second value. For example:
Vote.group('answer_id').group('column_1').order('column_1 DESC')
I just figured out a way to organize my links by vote count, using the sort method in my links view:
<% #user.links.sort { |a, b| b.votes.sum(:score) <=> a.votes.sum(:score) }.each do |link| %>
votes.sum(:score) grabs the number of votes a particular link has from the votes table.
hope that helps.

Resources