プロジェクト

全般

プロフィール

Bug(バグ) #3135

サブクエリのWHERE節に「field IN ?」の形式のDQLがあると Invalid parameter number エラーが発生する

Rimpei Ogawa11年以上前に追加. 約4年前に更新.

ステータス:
New(新規)
優先度:
Normal(通常)
担当者:
-
対象バージョン:
開始日:
2012-07-27
期日:
進捗率:

0%

3.6 で発生するか:
Yes (はい)
3.8 で発生するか:
Unknown (未調査)

説明

Overview (現象)

Doctrine でサブクエリの WHERE 節に filed IN ? の形式のものがあると、 DQL から SQL を生成する際に Invalid parameter number のエラーが発生する。

SELECT 〜 WHERE field = (SELECT field FROM table WHERE field IN ?);
SQLSTATE[HY093]: Invalid parameter number: number of bound variables does not match number of tokens

次のようにパラメーター数分の ? が DQL 時点で展開されている場合はエラーは発生しない。

SELECT 〜 WHERE field = (SELECT field FROM table WHERE field IN (?, ?, ?));

具体的には以下のようなコードでエラーが発生する。

例1 : サブクエリの生成に andWhereIn() を利用する場合

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();

例2 : サブクエリを手動で記述する場合

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回目以降のアクセスで成功していた

本問題は基本的には Doctrine のバグである可能性が高いが、例1 に関しては opDoctrineQuery を利用しない場合はエラーが発生しないため、OpenPNE の問題とも考えられる可能性がある。

本問題は #3052 の調査中に発見した。(#3052 は 例1 のケースに該当する)

Causes (原因)

DQL から SQL を生成するタイミングで filed IN ? の形式は、 field IN (?, ?, ?) のようにパラメーター配列の要素数に応じた形式に変換されるが、サブクエリ内の変換処理を行なう際にパラメーターを正しく扱えていないようで、要素数を間違ったり、パラメーターが展開されなかったりでエラーになる。

opDoctrineQuery::andWhereIn() は、パフォーマンスチューニングのため filed IN ? の形式の DQL を生成するが (#991)、親クラスの Doctrine_Query_Abstract::andWhereIn() をそのまま使う場合は DQL 時点でパラメーターの展開が行われるためこの問題が発生しない。

Way to fix (修正内容)

例1 だけを修正するのであれば、以下のようにサブクエリ利用前提の場合はパフォーマンスチューニングのためのコードを利用しないという修正案が考えられます。

--- 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)

ただし、この修正では例2は動作しないため修正としては不十分かもしれない。


関連するチケット

関連している OpenPNE 3 - Bug(バグ) #3052: activity/community.json 実行時にDoctrineでエラーが発生する場合がある Won't fix(対応せず) 2012-05-10
関連している OpenPNE 3 - Bug(バグ) #991: [Optimization] Doctrine_Query::andWhereIn() add inefficient conditions to a DQL (Doctrine_Query::andWhereIn() が非効率な条件を DQL に追加する) Fixed(完了) 2010-04-27
関連している OpenPNE 3 - Bug(バグ) #3273: activity/search.json?member_id=? で特定メンバーのアクティビティを取得すると500エラーとなる Fixed(完了) 2012-12-06
関連している OpenPNE 3 - Bug(バグ) #3338: Doctrine のサブクエリの中でエイリアスを指定した場合に正しくないSQLが発行される場合がある New(新規) 2013-05-07
関連している OpenPNE 3 - Bug(バグ) #3339: Doctrine で WHERE IN を用いたサブクエリの場合に配列が正しく展開されない New(新規) 2013-05-07

履歴

#1 Yuya Watanabeほぼ11年前に更新

  • 3.8 で発生するかUnknown (未調査) にセット

原因

test/unit/util/opActivityQueryBuilderTest.php のテストを動かしたときに下記のようなクエリが MySQL で受け取られている.ここで WHERE NOT IN の部分を見ると 'Array' という値が入っていることがわかる.

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_para
m 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 IN (SELECT m2.member_id_to AS m2__member_id_
to FROM member_relationship m2 WHERE (m2.member_id_from = '1' AND m2.is_friend = 1 AND m2.member_id_to NOT IN ('Array'))) AND a.public_flag $
= '2') AND (a.foreign_table IS NULL OR a.foreign_table <> "community") AND (m.is_active = '1' OR m.is_active IS NULL) ORDER BY a.id DESC

該当箇所をみてみると,配列かどうかについては特に確認していない.

178   public function andWhereIn($expr, $params = array(), $not = false)
179   {
180     if (isset($params) && (count($params) == 0))
181     {
182       if (!$not)
183       {
184         return $this->andWhere('0 = 1');
185       }
186       else
187       {
188         return parent::andWhereIn($expr, $params, $not);
189       }
190     } 
192     $this->addWhereInCount(count($params));
193 
194     if ($not)
195     {
196       $this->andWhere($expr.' NOT IN ?', array($params));
197     }
198     else
199     {
200       $this->andWhere($expr.' IN ?', array($params));
201     } 
202 
203     return $this;
204   }

原因としては,おそらく Doctrine_Query の andWhereIn() に渡される際にネストされない配列であるべきものがネストされて渡されていたためとおもわれる.
実際に下記のような修正を適用し, f9eaaaf97258be8b38d4b6e9e36d79195481d5fc の修正のみを revert したコードでテストを実行した場合にすべてのテストケースをパスした.

diff --git a/lib/util/opDoctrineQuery.class.php b/lib/util/opDoctrineQuery.class.php
index 7e5db73..fc9757e 100644
--- a/lib/util/opDoctrineQuery.class.php
+++ b/lib/util/opDoctrineQuery.class.php
@@ -191,13 +191,18 @@ class opDoctrineQuery extends Doctrine_Query

     $this->addWhereInCount(count($params));

+    if (!is_array($params))
+    {
+      $params = array($params);
+    }
+
     if ($not)
     {
-      $this->andWhere($expr.' NOT IN ?', array($params));
+      $this->andWhere($expr.' NOT IN ?', $params);
     }
     else
     {
-      $this->andWhere($expr.' IN ?', array($params));
+      $this->andWhere($expr.' IN ?', $params);
     }

     return $this;

追記)

これでもいいらしい

diff --git a/lib/util/opDoctrineQuery.class.php b/lib/util/opDoctrineQuery.class.php
index 7e5db73..106c051 100644
--- a/lib/util/opDoctrineQuery.class.php
+++ b/lib/util/opDoctrineQuery.class.php
@@ -193,11 +193,11 @@ class opDoctrineQuery extends Doctrine_Query

     if ($not)
     {
-      $this->andWhere($expr.' NOT IN ?', array($params));
+      $this->andWhere($expr.' NOT IN ?', (array)$params);
     }
     else
     {
-      $this->andWhere($expr.' IN ?', array($params));
+      $this->andWhere($expr.' IN ?', (array)$params);
     }

     return $this;

#2 Yuya Watanabeほぼ11年前に更新

note-1 の内容は例1や例2の修正としては不適切のようです.

#3 Yuya Watanabeほぼ11年前に更新

調査中に別のバグっぽい挙動を見つけたので報告しました.

Bug(バグ) #3338: Doctrine のサブクエリの中でエイリアスを指定した場合に正しくないSQLが発行される場合がある
https://redmine.openpne.jp/issues/3338

#4 Yuya Watanabeほぼ11年前に更新

調査中に別のバグっぽい挙動を見つけたので報告しました.

Bug(バグ) #3339: Doctrine で WHERE IN を用いたサブクエリの場合に配列が正しく展開されない
https://redmine.openpne.jp/issues/3339

#5 Youichi Kimura10年以上前に更新

  • 説明 を更新 (diff)

Doctrineに対して下記の修正を施すことで、概要に書かれているコードは動作するようになりました。他のSNS機能に影響がないかどうかは未確認です。
https://github.com/upsilon/doctrine1/compare/op3135_subquery-where-in-bug

#6 Youichi Kimura10年以上前に更新

OpenPNE で独自に note-5 のような修正を施すことは問題ありませんが、Doctrine1 は LGPL なのでプロプライエタリな利用で先行して note-5 の修正を取り込むことは避けた方が良いかもしれません。

#7 Yuya Watanabe10年以上前に更新

いろいろなところに影響が出ているので #991 の修正として追加されている opDoctrineQuqyer の andWhereIn() 自体を一旦削除してしまうというのも手かもしれない.

#8 kaoru n約4年前に更新

  • 対象バージョンOpenPNE 3.10.x にセット

他の形式にエクスポート: Atom PDF