RoR控制器中用原生SQL替换Book.all出现undefined method `title'报错的解决方案咨询
这个问题我之前也碰到过,本质原因是你用ActiveRecord::Base.connection.execute(sql)执行原生SQL后,返回的结果和Book.all的类型完全不同——前者是哈希对象的集合,后者是Book模型实例的集合。视图里你还是像之前那样调用@book.title(模型实例的方法),但哈希根本没有这个方法,所以才会报undefined method 'title' for #Hash:...的错误。
下面给你几种可行的解决办法,按推荐程度排序:
1. 使用find_by_sql(最推荐)
Rails专门提供了find_by_sql方法,它会自动把原生SQL的查询结果转换成对应模型的实例,完美衔接你原来的视图逻辑。修改后的控制器代码如下:
def index sql = 'SELECT * FROM books' @books = Book.find_by_sql(sql) end
这样@books里的每一项都是标准的Book模型实例,视图里的@book.title、@book.author之类的调用都能正常工作,和你用Book.all时完全一样。
2. 手动将哈希转换为模型实例
如果你一定要坚持用connection.execute,可以手动把返回的哈希转换成Book实例:
def index sql = 'SELECT * FROM books' results = ActiveRecord::Base.connection.execute(sql) # 用instantiate方法创建已持久化的模型实例(更贴合查询场景) @books = results.map { |row| Book.instantiate(row) } end
这里用Book.instantiate而不是Book.new,是因为它会把记录标记为已持久化(对应数据库里已存在的记录),同时正确设置主键等属性,比直接new更严谨。
3. 视图中改用哈希访问(不推荐)
如果不想修改控制器代码,也可以在视图里改成哈希的键值访问方式,比如:
<% @books.each do |book| %> <p>书名:<%= book['title'] %></p> <% end %>
但这种方法不推荐,因为它破坏了MVC的分层逻辑,而且如果后续模型有业务方法(比如格式化日期、计算价格),你就没法直接调用了,维护性会变差。
总结一下,优先用find_by_sql,它既满足了使用原生SQL的需求,又保留了ActiveRecord模型的所有便利特性。
内容的提问来源于stack exchange,提问作者Daryl Wong

