简体   繁体   中英

Rails Associations has_one Latest Record

I have the following model:

class Section < ActiveRecord::Base
  belongs_to :page
  has_many :revisions, :class_name => 'SectionRevision', :foreign_key => 'section_id'
  has_many :references

  has_many :revisions, :class_name => 'SectionRevision', 
                       :foreign_key => 'section_id'

  delegate :position, to: :current_revision

  def current_revision
    self.revisions.order('created_at DESC').first
  end
end

Where current_revision is the most recently created revision . Is it possible to turn current_revision into an association so I can perform query like Section.where("current_revision.parent_section_id = '1'") ? Or should I add a current_revision column to my database instead of trying to create it virtually or through associations?

To get the last on a has_many, you would want to do something similar to @jvnill, except add a scope with an ordering to the association:

has_one :current_revision, -> { order created_at: :desc }, 
  class_name: 'SectionRevision', foreign_key: :section_id

This will ensure you get the most recent revision from the database.

You can change it to an association but normally, ordering for has_one or belongs_to association are always interpreted wrongly when used on queries. In your question, when you turn that into an association, that would be

has_one :current_revision, class_name: 'SectionRevision', foreign_key: :section_id, order: 'created_at DESC'

The problem with this is that when you try to combine this with other queries, it will normally give you the wrong record.

>> record.current_revision
   # gives you the last revision
>> record.joins(:current_revision).where(section_revisions: { id: 1 })
   # searches for the revision where the id is 1 ordered by created_at DESC

So I suggest you to add a current_revision_id instead.

As @jvnill mentions, solutions using order stop working when making bigger queries, because order 's scope is the full query and not just the association.

The solution here requires accurate SQL:

  has_one  :current_revision, -> { where("NOT EXISTS (select 1 from section_revisions sr where sr.id > section_revisions.id and sr.section_id = section_revisions.section_id LIMIT 1)") }, class_name: 'SectionRevision', foreign_key: :section_id

I understand you want to get the sections where the last revision of each section has a parent_section_id = 1;

I have a similar situation, first, this is the SQL (please think the categories as sections for you, posts as revisions and user_id as parent_section_id -sorry if I don't move the code to your need but I have to go):

SELECT categories.*, MAX(posts.id) as M
FROM `categories` 
INNER JOIN `posts` 
ON `posts`.`category_id` = `categories`.`id` 
WHERE `posts`.`user_id` = 1
GROUP BY posts.user_id
having M = (select id from posts where category_id=categories.id order by id desc limit 1)

And this is the query in Rails:

Category.select("categories.*, MAX(posts.id) as M").joins(:posts).where(:posts => {:user_id => 1}).group("posts.user_id").having("M = (select id from posts where category_id=categories.id order by id desc limit 1)")

This works, it is ugly, I think the best way is to "cut" the query, but if you have too many sections that would be a problem while looping trough them; you can also place this query into a static method, and also, your first idea, have a revision_id inside of your sections table will help to optimize the query, but will drop normalization (sometimes it is needed), and you will have to be updating this field when a new revision is created for that section (so if you are going to be making a lot of revisions in a huge database it maybe would be a bad idea if you have a slow server...)

UPDATE I'm back hehe, I was making some tests, and check this out:

def last_revision
    revisions.last
end

def self.last_sections_for(parent_section_id)
  ids = Section.includes(:revisions).collect{ |c| c.last_revision.id rescue nil }.delete_if {|x| x == nil}

  Section.select("sections.*, MAX(revisions.id) as M")
         .joins(:revisions)
         .where(:revisions => {:parent_section_id => parent_section_id})
         .group("revisions.parent_section_id")
         .having("M IN (?)", ids)
end

I made this query and worked with my tables (hope I named well the params, it is the same Rails query from before but I change the query in the having for optimization); watch out the group; the includes makes it optimal in large datasets, and sorry I couldn't find a way to make a relation with has_one, but I would go with this, but also reconsider the field that you mention at the beginning.

If your database supports DISTINCT ON

class Section < ApplicationRecord
  has_one :current_revision, -> { merge(SectionRevision.latest_by_section) }, class_name: "SectionRevision", inverse_of: :section
end
class SectionRevision < ApplicationRecord
  belongs_to: :section
  scope :latest_by_section, -> do
    query = arel_table
      .project(Arel.star)
      .distinct_on(arel_table[:section_id])
      .order(arel_table[:section_id].asc, arel_table[:created_at].desc)
    revisions = Arel::Nodes::TableAlias.new(
      Arel.sql(format("(%s)", query.to_sql)), arel_table.name
    )
    from(revisions)
  end
end

It works with preloading

Section.includes(:current_revision)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM