Bug(バグ) #3052
完了activity/community.json 実行時にDoctrineでエラーが発生する場合がある
0%
説明
現象¶
activity/community.json 呼び出し時に下記のようなエラーがhttpdのエラーログに出力されて500エラーとなる。
SQLSTATE[HY093]: Invalid parameter number: number of bound variables does not match number of tokens
is_active: 0 のメンバー(仮登録中のメンバー)が1人以上いる場合に発生する。
原因¶
マイフレンドまで公開のアクティビティを取得するための条件の中で、is_active: 0 のメンバーを対象外とするためにサブクエリ内に member_id_to NOT IN ?
を追加していたが、この形式の DQL を Doctrine が正しくパースできず、最終的にパラメーター数が一致しない SQL とパラメーターの組が生成されていたためにエラーとなっていた。
inactiveIds が空の場合は opDoctrineQuery::andWhereIn() により ->andWhere('0 = 1');
に書き換えられるため問題が発生しない。
修正内容¶
問題箇所のクエリでは Member モデルを必ず leftJoin しているため、opActivateListener により is_active のチェックが自動的に追加される。
このクエリにおいて is_active: 0 のメンバーを除外するには Member に対する絞り込みのみで十分であるため、マイフレンドまで公開のアクティビティを抽出する部分からは is_active: 0 を除外する条件を冗長と判断し削除した。
Youichi Kimura さんが12年以上前に更新
- ステータス を New(新規) から Pending Review(レビュー待ち) に変更
- 進捗率 を 0 から 50 に変更
更新履歴 1d56bc54d7f4e96a7664e3792a99205ca1d6c6b3 で適用されました。
Yuya Watanabe さんが12年以上前に更新
activity/community.json を用いるために opTimelinePlugin を入れた状態で コミュニティホーム を閲覧するとチケットのエラーが発生し続ける場合がある.これは一度発生した OpenPNE では継続的に発生するものであると思われる.
エラーが発生するリクエストは下記URLである.ただし,下記URLは URL と apiKey を差し替えている.
http://sns.example.com/api.php/activity/search.json?count=20&since_id=&target=community&target_id=2&count=20&post%5Bforeign%5D=community&post%5BforeignId%5D=2¬ify%5Blib%5D=http%3A%2F%2Fsns.example.com%2FopTimelinePlugin%2Fjs%2Fjquery.desktopify.js¬ify%5Btitle%5D=%E3%81%B5%E3%81%9F%E3%81%A4%E3%82%81+%E3%81%AE%E6%9C%80%E6%96%B0%E6%8A%95%E7%A8%BF&timer=5000&apiKey=******
レスポンス
{"error":{"code":500,"message":"Internal Server Error"}}
Jul 06 22:07:44 symfony [info] {Doctrine_Connection_Statement} execute : SELECT m.id AS m__id, m.member_id AS m__member_id, m.name AS m__name, m.value AS m__value, m.value_datetime AS m__value_datetime, m.name_value_hash AS m__name_value_hash, m.created_at AS m__created_at, m.updated_at AS m__updated_at, m2.id AS m2__id, m2.name AS m2__name, m2.invite_member_id AS m2__invite_member_id, m2.is_login_rejected AS m2__is_login_rejected, m2.created_at AS m2__created_at, m2.updated_at AS m2__updated_at, m2.is_active AS m2__is_active FROM member_config m LEFT JOIN member m2 ON m.member_id = m2.id WHERE (m.value = ? AND (m2.is_active = ? OR m2.is_active IS NULL)) LIMIT 1 - (******, 1) Jul 06 22:07:44 symfony [info] {Doctrine_Connection_Statement} execute : SELECT m.id AS m__id, m.member_id AS m__member_id, m.name AS m__name, m.value AS m__value, m.value_datetime AS m__value_datetime, m.name_value_hash AS m__name_value_hash, m.created_at AS m__created_at, m.updated_at AS m__updated_at FROM member_config m WHERE (m.member_id = ?) - (1) Jul 06 22:07:44 symfony [info] {Doctrine_Connection_Statement} execute : SELECT a.id AS a__id, a.member_id AS a__member_id, a.in_reply_to_activity_id AS a__in_reply_to_activity_id, a.body AS a__body, a.uri AS a__uri, a.public_flag AS a__public_flag, a.is_pc AS a__is_pc, a.is_mobile AS a__is_mobile, a.source AS a__source, a.source_uri AS a__source_uri, a.foreign_table AS a__foreign_table, a.foreign_id AS a__foreign_id, a.template AS a__template, a.template_param AS a__template_param, a.created_at AS a__created_at, a.updated_at AS a__updated_at, m.id AS m__id, m.name AS m__name, m.invite_member_id AS m__invite_member_id, m.is_login_rejected AS m__is_login_rejected, m.created_at AS m__created_at, m.updated_at AS m__updated_at, m.is_active AS m__is_active FROM activity_data a LEFT JOIN member m ON a.member_id = m.id WHERE ((a.member_id = ? AND a.public_flag IN (?, ?, ?, ?)) OR (a.member_id IN (SELECT m2.member_id_to AS m2__member_id_to FROM member_relationship m2 WHERE (m2.member_id_from = ? AND m2.is_friend = 1 AND m2.member_id_to NOT IN (?, ?, ?, ?))) AND a.public_flag IN (?, ?)) OR a.public_flag IN (?, ?, ?)) AND a.foreign_table = "community" AND a.foreign_id = ? AND EXISTS (SELECT c.id AS c__id, c.community_id AS c__community_id, c.member_id AS c__member_id, c.is_pre AS c__is_pre, c.is_receive_mail_pc AS c__is_receive_mail_pc, c.is_receive_mail_mobile AS c__is_receive_mail_mobile, c.created_at AS c__created_at, c.updated_at AS c__updated_at FROM community_member c WHERE (c.member_id = a.member_id AND c.community_id = ?)) AND a.id > ? AND a.in_reply_to_activity_id IS NULL AND (m.is_active = ? OR m.is_active IS NULL) ORDER BY a.id DESC LIMIT 20 - (1, 3, 2, 1, 0, 1, 3, 4, 2, 1, 0, 1, 0, 2, 2, , 1) Jul 06 22:07:44 symfony [err] {Doctrine_Connection_Mysql_Exception} SQLSTATE[HY093]: Invalid parameter number: number of bound variables does not match number of tokens
また、他の環境では下記のようなSQLが構築されているようである.
Apr 26 18:06:28 symfony [info] {Doctrine_Connection_Statement} execute : SELECT a.id AS a__id, a.member_id AS a__member_id, a.in_reply_to_activity_id AS a__in_reply_to_activity_id, a.body AS a__body, a.uri AS a__uri, a.public_flag AS a__public_flag, a.is_pc AS a__is_pc, a.is_mobile AS a__is_mobile, a.source AS a__source, a.source_uri AS a__source_uri, a.foreign_table AS a__foreign_table, a.foreign_id AS a__foreign_id, a.template AS a__template, a.template_param AS a__template_param, a.created_at AS a__created_at, a.updated_at AS a__updated_at, m.id AS m__id, m.name AS m__name, m.invite_member_id AS m__invite_member_id, m.is_login_rejected AS m__is_login_rejected, m.created_at AS m__created_at, m.updated_at AS m__updated_at, m.is_active AS m__is_active FROM activity_data a LEFT JOIN member m ON a.member_id = m.id WHERE ((a.member_id = ? AND a.public_flag IN (?, ?, ?, ?)) OR (a.member_id IN (SELECT m2.member_id_to AS m2__member_id_to FROM member_relationship m2 WHERE (m2.member_id_from = ? AND m2.is_friend = 1 AND m2.member_id_to NOT IN (?, ?, ?, ?))) AND a.public_flag IN (?)) OR a.public_flag IN (?, ?, ?)) AND a.foreign_table = "community" AND a.foreign_id = ? AND EXISTS (SELECT c.id AS c__id, c.community_id AS c__community_id, c.member_id AS c__member_id, c.is_pre AS c__is_pre, c.is_receive_mail_pc AS c__is_receive_mail_pc, c.is_receive_mail_mobile AS c__is_receive_mail_mobile, c.created_at AS c__created_at, c.updated_at AS c__updated_at FROM community_member c WHERE (c.member_id = a.member_id AND c.community_id = ?)) AND a.in_reply_to_activity_id IS NULL AND (m.is_active = ? OR m.is_active IS NULL) ORDER BY a.id DESC LIMIT 20 - (1, 3, 2, 1, 0, 1, 2, 2, 1, 0, 1, 0, 2, 2, 1) Apr 26 18:06:28 symfony [err] {Doctrine_Connection_Mysql_Exception} SQLSTATE[HY093]: Invalid parameter number: number of bound variables does not match number of tokens
Rimpei Ogawa さんが12年以上前に更新
- ステータス を Pending Review(レビュー待ち) から Accepted(着手) に変更
- 担当者 を Youichi Kimura から Rimpei Ogawa に変更
- 進捗率 を 50 から 0 に変更
現時点での調査結果をコメントしておきます。
まず、 1d56bc54d7f4e96a7664e3792a99205ca1d6c6b3 の修正は a.member_id_to が存在しないフィールドを指しているため誤りです。
この問題は is_active: 0 のメンバーが1人以上いる場合に発生します。
->andWhereNotIn('r.member_id_to', $this->inactiveIds);
の行が原因ですが、inactiveIds が空の場合は opDoctrineQuery::andWhereIn() により ->andWhere('0 = 1');
に書き換えられるため問題が発生しません。
この行はマイフレンドまで公開のアクティビティを取得する際に、閲覧者のフレンドを特定する際に is_active: 0 のメンバーを除外するために記述されているものと思われます。しかし、このクエリでは Member を leftJoin しているので、opActivateBehavior により is_active のチェックが自動的に追加されるため、実質この行は不要と考えられます。
この箇所のみを問題とするのであれば、以下の修正で十分かと思います。
--- a/lib/util/opActivityQueryBuilder.class.php +++ b/lib/util/opActivityQueryBuilder.class.php @@ -155,8 +155,7 @@ class opActivityQueryBuilder ->select('r.member_id_to') ->from('MemberRelationship r') ->addWhere('r.member_id_from = ?', $member_id) - ->addWhere('r.is_friend = true') - ->andWhereNotIn('r.member_id_to', $this->inactiveIds); + ->addWhere('r.is_friend = true'); return $this->buildMemberQuery($query, $friendsQuery, ActivityDataTable::PUBLIC_FLAG_FRIEND); }
ただ、この andWhereNotIn() の記述自体は冗長ではありますが間違った使い方ではなく、根本的な問題は別にあります。
その問題は、Doctrine でサブクエリの中で filed IN ?
の形式が現れる以下のような DQL が正しく扱えないことです。
SELECT 〜 WHERE field = (SELECT field FROM table WHERE field IN ?);
DQL から SQL を生成するタイミングで filed IN ?
の形式は、 field IN (?, ?, ?)
のようにパラメーター配列の要素数に応じた形式に変換されるのですが、サブクエリ内の変換処理を行なう際にパラメーターを正しく扱えていないようで、要素数を間違ったり、パラメーターが展開されなかったりでエラーになります。
opDoctrineQuery::andWhereIn() は、パフォーマンスチューニングのため filed IN ?
の形式の DQL を生成しますが(#991)、Doctrine_Query_Abstract::andWhereIn() をそのまま使う場合は DQL 時点でパラメーターの展開が行われるためこの問題が発生しません。
サブクエリで利用される前提の場合には opDoctrineQuery::andWhereIn() は、 Doctrine_Query_Abstract::andWhereIn() をそのまま使う方がよいかもしれません。
--- a/lib/util/opDoctrineQuery.class.php +++ b/lib/util/opDoctrineQuery.class.php @@ -189,6 +189,11 @@ class opDoctrineQuery extends Doctrine_Query } } + if ($this->isSubquery()) + { + return parent::andWhereIn($expr, $params, $not); + } + $this->addWhereInCount(count($params)); if ($not)
この修正を行なうと以下のような記述は正しく動作します。
opActivateBehavior::disable(); // あってもなくてもエラー $q = Doctrine_Core::getTable('MemberConfig')->createQuery('c'); $q2 = $q->createSubquery() ->select('m.id') ->from('Member m') ->andWhereIn('m.id', array('1', '2')); $q->andWhere('c.member_id IN ('.$q2->getDql().')'); $q->execute();
一方、以下のような記述は依然として動作しません。
opActivateBehavior::disable(); // あってもなくてもエラー $q = Doctrine_Core::getTable('MemberConfig')->createQuery('c'); $q->andWhere('c.member_id IN (SELECT m.id FROM Member m WHERE m.id IN ?', array(array('1', '2'))); $q->execute();
ただし、上の2つの例は APC のキャッシュが有効な状態では2回目以降のアクセスで成功していました。
DQL パーサーの内部まで詳細に追えていないので根本的な問題を確実に修正する方法がわかっておらず、修正方針が定まっていないのが現状です。
Rimpei Ogawa さんが12年以上前に更新
- 説明 を更新 (差分)
- 3.6 で発生するか を Unknown (未調査) から No (いいえ) に変更
サブクエリに関する根本的な問題については対象バージョンも 3.8 以上ではなくなるため、別チケットで扱うことにします。Description に note-4 の前半の内容を反映します。
Rimpei Ogawa さんが12年以上前に更新
- ステータス を Accepted(着手) から Pending Review(レビュー待ち) に変更
- 進捗率 を 0 から 50 に変更
更新履歴 15c865e761d1a1b84cef36b589c4508d470e534b で適用されました。
Yuya Watanabe さんが12年以上前に更新
- ステータス を Pending Review(レビュー待ち) から Pending Testing(テスト待ち) に変更
- 進捗率 を 50 から 70 に変更