ActiveRecord#first方法总是以最小ID返回记录吗?

时间:2015-11-25 13:29:47

标签: postgresql ruby-on-rails-4 activerecord

环境:Rails 4.2.4,Postgres 9.4.1.0

是否可以保证ActiveRecord#first方法始终返回具有最小ID和ActiveRecord#last的记录 - 具有最大ID?

我可以从Rails控制台看到,对于这两种方法,适当的ORDER ASC/DESC被添加到生成的SQL中。但另一个SO帖子Rails with Postgres data is returned out of order的作者告诉first方法返回 NOT 第一条记录......

首先是ActiveRecord:

  

2.2.3:001> Account.first
  帐户加载(1.3毫秒)SELECT"帐户"。* FROM"帐户" ORDER BY" accounts"。" id" ASC LIMIT 1

ActiveRecord last:

  

2.2.3:002> Account.last
  帐户加载(0.8毫秒)SELECT"帐户"。* FROM"帐户" ORDER BY" accounts"。" id" DESC LIMIT 1

==========

稍后添加:

所以,我做了自己的调查(基于 D-side 答案),答案是。一般来说,唯一的保证是first方法将从集合中返回第一个记录。它可能作为副作用向SQL添加ORDER BY PRIMARY_KEY条件,但它取决于是否已将任何记录加载到缓存/内存中。

这里从Rails 4.2.4中提取方法: 的 /activerecord/lib/active_record/relation/finder_methods.rb

# Find the first record (or first N records if a parameter is supplied).
# If no order is defined it will order by primary key.
# ---> NO, IT IS NOT. <--- This comment is WRONG.

def first(limit = nil)
  if limit
    find_nth_with_limit(offset_index, limit)
  else
    find_nth(0, offset_index) # <---- When we get there - `find_nth_with_limit` method will be triggered (and will add `ORDER BY`) only when its `loaded?` is false
  end
end


def find_nth(index, offset)
  if loaded?
    @records[index] # <--- Here's the `problem` where record is just returned by index, no `ORDER BY` is applied to SQL

  else
    offset += index
    @offsets[offset] ||= find_nth_with_limit(offset, 1).first
  end
end

以下是一些明确的例子:

Account.first #  True, records are ordered by ID

a = Account.where('free_days > 1') # False, No ordering
a.first # False, no ordering, record simply returned by @records[index]

Account.where('free_days > 1').first # True, Ordered by ID

a = Account.all # False, No ordering    
a.first # False, no ordering, record simply returned by @records[index]

Account.all.first # True,  Ordered by ID

现在有很多关系的例子:

帐户has_many AccountStatuses,AccountStatus belongs_to帐户

a = Account.first
a.account_statuses # No ordering

a.account_statuses.first
# Here is a tricky part: sometimes it returns @record[index] entry, sometimes it may add ORDER BY ID (if records were not loaded before)

以下是我的结论: 将方法first视为从已加载的集合中返回第一个记录(可以在任何顺序中加载,即无序)。如果我想确保first方法将返回具有最小ID的记录 - 那么我应该在之前适当地订购我应用first方法的集合。

关于first方法的Rails文档是错误的,需要重写。 http://guides.rubyonrails.org/active_record_querying.html

  

1.1.3第一次

     

第一种方法查找主键排序的第一条记录。 &lt; ---不,不是!

3 个答案:

答案 0 :(得分:1)

它可能会根据你的数据库引擎而改变,它总是使用first方法返回mysql中的最小ID但是对于postgresql它不起作用,当我是一个nobai时,我遇到了几个问题,我的应用程序在本地使用mysql正常工作,但是当使用postgresql部署到heroku时,一切都搞砸了,所以为了避免postgresql的问题,总是在查询之前通过id命令你的记录:

Account.order(:id).first

以上确保mysql,postgresql和任何其他数据库引擎的最小ID,如您在查询中所见:

SELECT  `accounts`.* FROM `accounts`  ORDER BY `accounts`.`id` ASC LIMIT 1

答案 1 :(得分:1)

  

如果未选择排序,则行将以未指定的方式返回   为了即可。在这种情况下的实际顺序将取决于扫描和连接   计划类型和磁盘上的订单,但不得依赖。一个   只有排序步骤才能保证特定的输出顺序   明确选择。

     

http://www.postgresql.org/docs/9.4/static/queries-order.html(强调我的)

因此,ActiveRecord实际上通过主键(无论哪个)添加排序,以保持结果的确定性。使用pry很容易找到相关的源代码,但这里是Rails 4.2.4的摘录:

# show-source Thing.all.first
def first(limit = nil)
  if limit
    find_nth_with_limit(offset_index, limit)
  else
    find_nth(0, offset_index)
  end
end

# show-source Thing.all.find_nth
def find_nth(index, offset)
  if loaded?
    @records[index]
  else
    offset += index
    @offsets[offset] ||= find_nth_with_limit(offset, 1).first
  end
end

# show-source Thing.all.find_nth_with_limit    
def find_nth_with_limit(offset, limit)
  relation = if order_values.empty? && primary_key
               order(arel_table[primary_key].asc) # <-- ATTENTION
             else
               self
             end

  relation = relation.offset(offset) unless offset.zero?
  relation.limit(limit).to_a
end

答案 2 :(得分:0)

我不认为您引用的答案是相关的(甚至是它所涉及的问题),因为它指的是无序查询,而firstlast确实应用了订单基于id

在某些情况下,如果您在查询中应用自己的group,则无法使用firstlast,因为如果分组不包含{,则无法应用订单{1}},但您可以使用id代替返回第一行。

take和/或first没有应用订单的版本(我回忆起PostgreSQL上已故的Rails 3之一),但它们都是错误。