基本的にはRailsでの話ですが、Postgresqlなどの高度なRDBを使用している場合はいずれもこの解決方法で問題ないでしょう。置き換えて呼んでください。
もともと一つだったアプリケーションが大きくなってきて2つのアプリケーションに分けたが、時間がなくてDBまで分けることができなかった場合、2つのアプリケーションが1つのRDBを使用することになります。
アプリケーションが動作しているプロセス以外からRDBに定義の変更を加えると、トランザクション内部でプリペアドステートメントが発生する場合にエラーになります。
このエラーはこんなシチュエーションの時に発生します。
このエラーの起こし方
まずはこのエラーの起こし方を示します。
前準備
プロセス1(アプリケーションの作成〜プリペアドステートメントの作成)
クリックするとAsciinemaのページへ遷移します。実際の操作を見ることができます。
アプリケーションとPostモデルの作成
# ユーザとロールの作成
createuser sample_app --superuser
# アプリケーションの作成(DBMSにはpostgresqlを指定)
rails new sample_app -d postgresql; cd sample_app
# Postモデルの作成
bundle exec rails g model Post title
# DBの作成とマイグレーション
bundle exec rake db:create db:migrate
# コンソールを立ち上げる
bundle exec rails c
プリペアドステートメントの作成
> Post.first
Post Load (0.6ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> nil
これでプリペアドステートメントが準備されました。
> ActiveRecord::Base.connection.execute('select * from pg_prepared_statements').values
(4.1ms) select * from pg_prepared_statements
=> [["a1", "SELECT \"posts\".* FROM \"posts\" ORDER BY \"posts\".\"id\" ASC LIMIT $1", "2017-05-24 18:35:44.401647+00", "{bigint}", false]]
次のステップでこのプロセスを使用するのでquitせずにそのままにしておいてください。
プロセス2(DBに変更を加える)
クリックするとAsciinemaのページへ遷移します。実際の操作を見ることができます。
別プロセスでpostsテーブルに変更を加える
なんでもいいですが、ここでは簡単にbodyカラムを追加します。
# 別プロセスで(新しくTerminalを開くとかしてください)
bundle exec rails db
# ALTER TABLE posts ADD body text;
ALTER TABLE
プロセス1(エラー発生)
クリックするとAsciinemaのページへ遷移します。実際の操作を見ることができます。
cached plan must not change result typeを発生させる
トランザクション内でさっき実行したPost.first
を実行します。
> Post.transaction {Post.first}
(0.4ms) BEGIN
Post Load (1.3ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT $1 [["LIMIT", 1]]
(0.3ms) ROLLBACK
ActiveRecord::PreparedStatementCacheExpired: ERROR: cached plan must not change result type
発生しました。
プリペアドステートメント
最近の高機能なRDBはプリペアドステートメントという機能を持ち合わせています。
これは実行するクエリの解析結果をキャッシュし、処理を高速化させる目的のものです。
それ以外にもSQLインジェクション対策になったりもします。
データベースは次の流れでSQLを実行します。
- SQLの構文・権限チェック
- 検索手法の決定・SQLのコンパイル(ここでプリペアドステートメントとしてキャッシュします)
- 検索・更新等を実行
複雑なクエリの場合にはこの威力は絶大です。
原因は?
PostgreSQLの場合、テーブルに定義的な変更が加わった場合、プリペアドステートメントの再作成を必須としています。
この再作成をしていなかったことに原因があります。
Railsはこの再作成をしていない場合、自動で再作成してくれる機能があります。
しかし、トランザクションの中でこのエラーが発生すると、エラー発生時点(プリペアドステートメント実行時)から先の処理がすべてキャンセルされるのでこの機能も使用できません。
解決方法
これは コネクションのキャッシュをクリアする ことで解決します。
もしくはプリペアドステートメントを使用しないという選択肢もあります。
複雑なSQLを使用しないなどの場合はそれでも問題ないでしょう。
コネクションのキャッシュをクリアするためにはすべてのアプリケーションで
- コネクションのキャッシュを直接削除する
- コネクションを再作成する
- アプリケーションを再起動する(コネクションが切れるので)
のいずれかをすると解決します。
コネクションのキャッシュを直接削除する
irb(main):002:0> ActiveRecord::Base.connection.execute('select * from pg_prepared_statements;').values
(0.5ms) select * from pg_prepared_statements;
=> [["a1", "SELECT \"posts\".* FROM \"posts\" ORDER BY \"posts\".\"id\" DESC LIMIT $1", "2017-05-28 11:04:31.670652+00", "{bigint}", false]]
irb(main):003:0> ActiveRecord::Base.connection.clear_cache!
=> {}
irb(main):004:0> ActiveRecord::Base.connection.execute('select * from pg_prepared_statements;').values
(0.5ms) select * from pg_prepared_statements;
=> []
irb(main):005:0>
コネクションを再作成する
irb(main):011:0> ActiveRecord::Base.connection.execute('select * from pg_prepared_statements;').values
(0.6ms) select * from pg_prepared_statements;
=> [["a3", "SELECT \"posts\".* FROM \"posts\" ORDER BY \"posts\".\"id\" DESC LIMIT $1", "2017-05-28 11:06:18.709812+00", "{bigint}", false]]
irb(main):012:0> ActiveRecord::Base.connection.reconnect!
=> []
irb(main):013:0> ActiveRecord::Base.connection.execute('select * from pg_prepared_statements;').values
(1.0ms) select * from pg_prepared_statements;
=> []
irb(main):014:0>
アプリケーションを再起動する
いつも通り再起動