Rails ActiveRecord字符串字段编码与Ruby String编码

das*_*s-g 5 ruby postgresql ruby-on-rails character-encoding rails-activerecord

上下文:从外部源代码转换字符串以保存在数据库中

从gem,我得到一个字符串s,它具有latin-1-encoded内容,我想存储在Rails模型中.

r = MyRecord.new(mystring: s)
# ...
r.save
Run Code Online (Sandbox Code Playgroud)

因为我的PostgreSQL数据库使用UTF-8编码,所以在将字符串字段设置为字符串后保存模型会在该字符串包含某些非ASCII字符时导致错误:

ActiveRecord::StatementInvalid: PG::CharacterNotInRepertoire: ERROR:  invalid byte sequence for encoding "UTF8": 0xdf 0x65
...
Run Code Online (Sandbox Code Playgroud)

我可以通过转码字符串轻松解决这个问题:

r = MyRecord.new(mystring: s.encode(Encoding::UTF_8, Encoding::ISO_8859_1))
# ...
r.save
Run Code Online (Sandbox Code Playgroud)

(因为r.encoding返回#<Encoding:ASCII-8BIT>而不是#<Encoding:ISO-8859-1>,我将源代码作为第二个参数传递.生成的gem s可能不知道它读取字符串的文件是否被latin1编码.)

挑战:避免硬编码目标编码

在我看来,关于数据库的字符串编码的知识不属于我执行此操作的代码部分,因此也不属于代码转换.

我可以向模型的类询问数据库的编码:

MyRecord.connection.encoding
Run Code Online (Sandbox Code Playgroud)

但这并不返回RubyEncoding对象,它返回一个包含编码名称的字符串.幸运的是,可以使用名称(和一些别名)查询Encoding类以查找编码:

Encoding.find 'UTF-8' # returns #<Encoding:UTF-8>, the value of Encoding::UTF_8
Run Code Online (Sandbox Code Playgroud)

不幸的是,使用了不同的命名约定: MyRecord.connection.encoding返回'UTF8'(没有减号),同时Encoding.find(...)需要传递'UTF-8'(减号)或者'CP65001'我们希望它返回#<Encoding:UTF-8>.)

Sooooo关闭.

问题:是否有干净和/或推荐的方式

避免目标编码的硬编码,而是动态地确定和使用数据库的编码?

丢弃的想法

我不觉得对结果MyRecord.connection.encoding或内容进行字符串操作或模式匹配Encoding.aliases()会比在代码中保留硬编码值更好.

修改Encoding.aliases()的返回值没有任何影响:

Encoding.aliases['UTF8'] = 'UTF-8'
Encoding.find 'UTF8' # ArgumentError: unknown encoding name - UTF8
Run Code Online (Sandbox Code Playgroud)

(并且无论如何也感觉不对),也没有修改以下的返回值#names:

Encoding::UTF_8.names.push('UTF8')
Encoding.find 'UTF8'# ArgumentError: unknown encoding name - UTF8
Run Code Online (Sandbox Code Playgroud)

我猜两者都只返回动态生成的集合或底层集合的副本,这是有充分理由的.

Den*_*ton 3

解决此问题的最简单且可以说是最干净的解决方案是不直接调用Encoding.find,而是使用一个实用程序方法(可能位于位于 的模块中lib/yourapp),该方法了解您关心的编码名称差异并回退到Encoding.find所有其他输入:

module YourApp
  module DatabaseStringEncoding
    def find(name)
      case name
      when 'UTF8'
        Encoding::UTF_8
      ...
      else
        Encoding.find(name)
      end 
    end
  end
Run Code Online (Sandbox Code Playgroud)

这既易于理解又易于发现(与直接修改相反Encoding,直接修改对于执行编码的代码的读者来说是不可见的)。基于这样的find方法,您可以进一步实现一种使用YourRecord.connection.encoding.

我知道完全按照自己的意愿去做会更令人兴奋Encoding.find,但我认为这种“愚蠢”的方法实际上是更好的方法。:-)