その他
    ホーム 技術発信 DoRuby RailsでOracle~ 導入と文字列の扱いについて
    RailsでOracle~ 導入と文字列の扱いについて
     

    RailsでOracle~ 導入と文字列の扱いについて

    この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

    Ruby on RailsからOracleデータベースを利用する際のtipsについて紹介します

    設定について

    ActiveRecordでOracleを扱うには
    activerecord-oracle_enhanced-adapter
    というgemを使います

    Gemfileには下記のように記述します

    gem 'activerecord-oracle_enhanced-adapter'
    

    config/database.ymlのアダプタの項目に下記のように指定します

    default: &default
      adapter: oracle_enhanced
    

    デフォルトではプライマリキーのシーケンスが10000から始まる(!)ようになっているので、他のDBアダプタと同じように1から始まるように変更します (参照: adapter-settings

    config/initializers/oracle.rb

    ActiveSupport.on_load(:active_record) do
      ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
        self.default_sequence_start_value = "1 NOCACHE INCREMENT BY 1"
      end
    end
    

    文字列の扱いについて

    空文字列を検索しづらい件

    Rails側でマイグレーション時に string と指定したカラムは、OracleではVARCHAR2型として定義されます。

    Oracleに詳しい方には常識ですが、このVARCHAR2型のカラムには
    「長さがゼロの文字列(空文字列)はNULLとみなされる」
    という他のRDBMSと違う大きな落とし穴があります。

    そのため、WHERE条件にVARCHAR2型のカラムを含める場合、カラムの値が ” の行を検索したいとき

    SELECT * FROM tbl1 WHERE col1 = '';
    

    こういうSQLを投げると、 WHERE col1 = NULL と解釈されてヒットしません。
    Rails側のコードでいうと

    str = ""
    Tbl.where(col: str)
    

    これがだめで、 str が ” ならば必ず nil に置き換える必要があります。

    pry(main)> str = ""
    => ""
    pry(main)> Customer.where(name: str).count
       (0.6ms)  SELECT COUNT(*) FROM "CUSTOMERS" WHERE "CUSTOMERS"."NAME" = :a1  [["name", ""]]
    => 0
    pry(main)> Customer.where(name: str.presence || nil).count
       (0.7ms)  SELECT COUNT(*) FROM "CUSTOMERS" WHERE "CUSTOMERS"."NAME" IS NULL
    => 1
    

    カラムがVARCHAR2の場合は ” を nil に置き換えてクエリを投げる、くらいのことはアダプタ側でやってほしいですね。

    CLOB型のWHERE検索が難しい

    Rails側でマイグレーション時に text と指定したカラムは、OracleではCLOB型として定義されます。

    SQLレベルではCLOB型の値とVARCHAR2型の値には互換性がなく、暗黙のキャストが行われないという他のRDBMSとは異なる不便な特徴があります。

    シングルクオートを使った文字列リテラルはOracleのSQL構文においてVARCHAR2型の値となるため、CLOB型のカラムをWHERE条件に指定しようとすると、SQLエラーになります。

    pry(main)> Customer.where(memo: 'hoge').to_a
      Customer Load (2.0ms)  SELECT "CUSTOMERS".* FROM "CUSTOMERS" WHERE "CUSTOMERS"."MEMO" = :a1  [["memo", #<OCI8::CLOB:0x00558008772008>]]
    ActiveRecord::StatementInvalid: OCIError: ORA-00932: データ型が一致しません: -が予想されましたがCLOBです。: SELECT "CUSTOMERS".* FROM "CUSTOMERS" WHERE "CUSTOMERS"."MEMO" = :a1
    

    試行錯誤の末、こういう式で通るようになりましたが、当然ながらインデックスは効きません。

    pry(main)> Customer.where('DBMS_LOB.SUBSTR(memo,1000,1) = ?', 'hoge').to_a
      Customer Load (5.5ms)  SELECT "CUSTOMERS".* FROM "CUSTOMERS" WHERE (dbms_lob.substr(memo, 1000, 1) = 'hoge')
    => [#<Customer:0x0055800a7de210 (後略
    

    まとめ

    RailsからOracleデータベースを利用する場合、文字列の扱いなど他のRDBMSにはない特殊な特性を把握して、注意しながらコーディングしていく必要があります。
    「こう書けば普通に動くだろう」という予断を置かず、実際にこまめに実験して想定通りの結果が得られることを確認しながら進めていきましょう。

    次回はOracle Textを使った日本語全文検索など、高度なトピックについて触れていく予定です。