Heroku部署后strftime函数报错,如何改写SQL查询语句?
这个问题太常见了——开发环境用SQLite,Heroku生产环境用PostgreSQL,两者的日期处理函数完全不一样:SQLite依赖strftime格式化日期,但PostgreSQL根本没有这个函数,自然会抛出function strftime(unknown, timestamp without time zone) does not exist的错误。下面给你几个实用的改写方案,从快速修复到优雅的跨环境兼容都有:
方案1:直接适配PostgreSQL(最快解决)
直接把SQLite的strftime('%Y%m', created_at)替换成PostgreSQL的to_char(created_at, 'YYYYMM'),这是PG里格式化日期为字符串的标准函数。同时我帮你把原来的字符串拼接改成参数绑定,避免SQL注入风险:
改写后的@microposts查询
@microposts = Micropost.where("to_char(created_at, 'YYYYMM') = ?", params[:yyyymm]) .order(:id) .page(params[:page])
改写后的@archives查询
@archives = Micropost.group("to_char(created_at, 'YYYYMM')") .order("to_char(created_at, 'YYYYMM') DESC") .count
方案2:兼容SQLite和PostgreSQL(跨环境通用)
如果不想切换开发环境的数据库,可以让代码自动识别当前数据库类型,适配不同的函数:
# 处理@microposts if ActiveRecord::Base.connection.adapter_name == 'SQLite' @microposts = Micropost.where("strftime('%Y%m', created_at) = ?", params[:yyyymm]) else @microposts = Micropost.where("to_char(created_at, 'YYYYMM') = ?", params[:yyyymm]) end @microposts = @microposts.order(:id).page(params[:page]) # 处理@archives if ActiveRecord::Base.connection.adapter_name == 'SQLite' @archives = Micropost.group("strftime('%Y%m', created_at)").order("strftime('%Y%m', created_at) DESC").count else @archives = Micropost.group("to_char(created_at, 'YYYYMM')").order("to_char(created_at, 'YYYYMM') DESC").count end
方案3:用Rails日期范围查询(最优雅,无原生SQL)
完全抛弃原生SQL,用Rails自带的日期范围查询,既安全又兼容所有数据库:
改写后的@microposts查询
# 先把参数转成日期,再取当月的起止时间 month_start = Date.parse(params[:yyyymm]).beginning_of_month month_end = Date.parse(params[:yyyymm]).end_of_month @microposts = Micropost.where(created_at: month_start..month_end) .order(:id) .page(params[:page])
这种写法没有SQL注入风险,而且Rails会自动根据数据库类型生成对应的SQL,SQLite和PostgreSQL都能完美运行。
改写后的@archives查询
如果想优雅统计每月帖子数,可以用Arel构建跨数据库的分组逻辑,或者引入groupdate gem简化代码:
# 用Arel实现跨数据库分组 month_column = Micropost.arel_table[:created_at] if ActiveRecord::Base.connection.adapter_name == 'SQLite' month_expr = month_column.strftime('%Y%m') else month_expr = month_column.to_char('YYYYMM') end @archives = Micropost.group(month_expr) .order(month_expr.desc) .count
或者引入groupdate gem(添加到Gemfile后执行bundle install),代码会更简洁:
@archives = Micropost.group_by_month(:created_at, format: "%Y%m").count.sort.reverse.to_h
最后提个小建议:开发环境尽量和生产环境用相同的数据库(PostgreSQL),能避免很多这类环境差异的坑,Heroku也有本地PostgreSQL的配置指南,你可以试试。
内容的提问来源于stack exchange,提问作者Yusuke Mori

