Bug(バグ) #1527
完了OpenPNE 2 からのアップグレードで、誤ったコミュニティカテゴリの紐付けをおこなってしまうことがある
100%
説明
Overview (現象)¶
OpenPNE 2 からのアップグレードで、誤ったコミュニティカテゴリの紐付けをおこなってしまうことがある。
この誤ったカテゴリの紐付けによって、中カテゴリにコミュニティがぶら下がってしまったり、 OpenPNE 3 からは閲覧できないコミュニティなどが生まれてしまう。
なお、この現象は OpenPNE 2 側データで「重複したコミュニティ名を持つコミュニティ」が存在している場合は発生しない(たとえば、 OpenPNE 公式 SNS には重複したコミュニティ名が存在していたため、この現象が発生しなかった)。
再現バージョン¶
- OpenPNE2.12.12 → 3.4.6
Causes (原因)¶
重複するコミュニティ名が存在しなかった場合に発行される、 OpenPNE 3 から OpenPNE 2 へのコミュニティインポート用の SQL に誤りがあった(この誤りは 2f36df96135741e537042bb028353bd1b90d83b8 で修正された)。
OpenPNE 2 からコミュニティを取得する SQL の WHERE 節の c_commu_category_id に指定される ID は、 OpenPNE 2 のコミュニティカテゴリ ID であるべきであるにも関わらず、 OpenPNE 3 のコミュニティカテゴリ ID を指定してしまっていた。
OpenPNE 2 のコミュニティカテゴリと OpenPNE 3 のコミュニティカテゴリはデータ構造が異なっており、 OpenPNE 2 では別テーブルであった中カテゴリも小カテゴリと同一のテーブルに格納される。そのため、小カテゴリの ID を OpenPNE 2 のものから意図的にずらしている。また、 OpenPNE 2 と OpenPNE 3 のどちらも、コミュニティは中カテゴリではなく小カテゴリに属するような実装になっている。
そこで、コンバータは小カテゴリに属するコミュニティ群のみを取得しにいこうとする。しかし、この誤った SQL では、コンバート対象とする小カテゴリIDの一覧は OpenPNE 3 のものであったので、 OpenPNE 2 では小カテゴリの ID だが OpenPNE 3 では中カテゴリの ID であるものはインポート対象にされていなかった。
Way to fix (修正内容)¶
1. まず、「原因」の項に示した問題を修正するために、重複するコミュニティ名が存在しなかった場合に発行する SQL でも、 OpenPNE 2 のカテゴリ ID を基にコミュニティのインポートをおこなうように修正した
2. 「1.」の修正はこれからコンバートをおこなう SNS にのみ有効なもので、既にコンバートしてしまった SNS が対象になるものではないので、このような SNS への救済措置として openpne:fix-wrong-categorized-community タスクを用意した
openpne:fix-wrong-categorized-community タスクがおこなうこと¶
- OpenPNE 2 の c_commu テーブルのうち、 OpenPNE 3 の community テーブルに移行されていないコミュニティを community テーブルにインポートする
- 「OpenPNE 3 の community テーブルに移行されていないコミュニティ」であるかどうか、つまり「OpenPNE 3 に正しく移行されたが削除されたコミュニティ」でないかどうかは、「community_member テーブルのデータの有無」によって区別する。アップグレード時には community テーブルに移行されなかった community_member の情報であってもすべて移行される。一方で、コミュニティの削除がおこなわれた場合は community_member の情報もまた削除されるため、 community_member のレコードが存在しているか否かによって「OpenPNE 3 に移行されていない」か「移行されたが削除された」かを判別することができる
- OpenPNE 2 と同名の中カテゴリ内に存在する同名の小カテゴリに属するコミュニティのみ、 OpenPNE 3 の正しいコミュニティカテゴリに紐付け直す
※タスクの実行中に手動で修復することを選択した場合は、上であげた処理と同等の SQL を画面に出力する。
※OpenPNE 3 からのコンバート後に名前を変更していたり、同じ名前でも属する中カテゴリが異なる小カテゴリについては、カテゴリの紐付け直しはおこなわない
報告元¶
http://sns.openpne.jp/diary/24998から転載
OpenPNE2.12.12から3.4.6へコンバートする際に、 OpenPNE2に存在していた「中カテゴリ」がそのままカテゴリの一種としてコンバートされてしまい、結果としてIDがOpenPNE2とOpenPNE3とではずれてしまう(当方では中カテゴリ一つだったので1づつずれている)。 結果、コミュニティ表示のカテゴリはまったく違うものとなる。 また、OpenPNE2で先頭のIDを持つコミュは消えてしまう (どうやらコンバート時にOpenPNE3用のテーブルにはコンバートされていない様子)
コメントにもバグに関する情報がありましたので追記します。
中カテゴリのコンバートをしないように opUpgradeFrom2ImportCommunityCategoryStrategy.class.php の61行目 $this->conn->execute($newParentSQL.' '.$oldParentSQL, array($parent['c_commu_category_parent_id'])); をコメントアウトしたところ、一応こちらの望むようにコンバートできました。 ただし、中カテゴリが1つのみの環境で行ったので、それ以上ある場合などの動作は不明です。
Kousuke Ebihara さんが約14年前に更新
http://github.com/ebihara/test-openpne-upgrade-from-2 のテストコードをベースに再現を試みましたが、正常にすべてのコミュニティとコミュニティカテゴリがコンバートされていました。引き続き調査します。
Kousuke Ebihara さんが約14年前に更新
- 優先度 を Normal(通常) から High(高め) に変更
- 対象バージョン を OpenPNE 3.6beta6 にセット
再現確認は済んでいませんが、この問題が存在するとすれば致命的です。優先度を引き上げて取り組みます。
Hiroki Mogi さんが約14年前に更新
2.14.7.3 → 3.6beta6 のアップグレードを行った環境で再現を確認しました。
この環境では上記のバグは再現できませんでした。
pnetan さんが約14年前に更新
報告者の方から追加で情報を頂きました。
返信が遅くなり申し訳ございません。 該当の日記にも書いておりますが、当方では次のような最小環境で再現を確認しました。 ・OpenPNE 2.12.12 を新規にインストールする(DBも新規に作成) ・コミュニティカテゴリを追加する。今回はデフォルトのカテゴリと、カテゴリ2という二つのカテゴリを作成しました。 ・それぞれのコミュニティカテゴリにコミュニティを作成する。コミュニティ数は適当だったと思います。 ・OpenPNE3.4.6を新規にインストールし、前述の環境からupgradeする 以上で再現しました。 OSはVMware上のcentos 5.5、 mysql は5.0.77(デフォルトのリポジトリを使用して、 yum install mysqlで入るバージョンです) phpは5.2.14をソースよりコンパイルしたものを使用しています つたない情報で申し訳ございませんが、何かの参考になれば。 以上です。失礼します。
Kousuke Ebihara さんが13年以上前に更新
作成中の差分です。このパッチを適用することでこのバグは修正されますが、誤ってインポートしたコミュニティの復旧はまだおこなえません。
diff --git a/data/upgrade/2/opUpgradeFrom2ImportCommunityCategoryStrategy.class.php b/data/upgrade/2/opUpgradeFrom2ImportCommunityCategoryStrategy.class.php index bcb1e17..6a96c2d 100644 --- a/data/upgrade/2/opUpgradeFrom2ImportCommunityCategoryStrategy.class.php +++ b/data/upgrade/2/opUpgradeFrom2ImportCommunityCategoryStrategy.class.php @@ -97,7 +97,7 @@ class opUpgradeFrom2ImportCommunityCategoryStrategy extends opUpgradeSQLImportSt } else { - $this->conn->execute($baseInsert.' '.$baseSelect.')', array($categoryId)); + $this->conn->execute($baseInsert.' '.$baseSelect.')', array($child['c_commu_category_id'])); } } }
Kousuke Ebihara さんが13年以上前に更新
- ステータス を New(新規) から Accepted(着手) に変更
バックポート先チケットにしか記述していなかった以下の情報をこちらにも転記します。
重複するコミュニティ名がなかった場合に実行されるコミュニティインポート用のSQLが誤っていた。 OpenPNE 2 からコミュニティを取得する SQL の WHERE 句の c_commu_category_id に指定される ID は、 OpenPNE 2 のコミュニティカテゴリ ID であるべきであるにも関わらず、 OpenPNE 3 のコミュニティカテゴリ ID を指定してしまっていた。 OpenPNE 2 のコミュニティカテゴリと OpenPNE 3 のコミュニティカテゴリはデータ構造が異なっており、 OpenPNE 2 では別テーブルであった中カテゴリも小カテゴリと同一のテーブルに格納される。そのため、小カテゴリの ID を OpenPNE 2 のものから意図的にずらしている。また、 OpenPNE 2 と OpenPNE 3 のどちらも、コミュニティは中カテゴリではなく小カテゴリに属するような実装になっている。 コンバータは小カテゴリに属するコミュニティ群のみを取得しにいこうとする。しかし、このときのコンバート対象とする小カテゴリIDの一覧は OpenPNE 3 のものであるので、 OpenPNE 2 では小カテゴリの ID だが OpenPNE 3 では中カテゴリの ID であるものはインポート対象にされていなかった。
既に OpenPNE 2 から OpenPNE 3.4 へのアップグレードを実施してしまい、コミュニティとカテゴリの紐付けが正しくおこなえていない SNS を修復するためのアップグレード手順も追加します。
既にコンバート済みのデータの修復を以下のようにすることを考えています。 * 残してある OpenPNE 2 のコミュニティとカテゴリの状態から、誤ったコンバートをしていないかどうか推定する * 誤ったコンバートをしていた場合、*OpenPNE 2 由来のコミュニティとカテゴリについてのみ*、データの修復を試みる
Kousuke Ebihara さんが13年以上前に更新
アップグレードタスクで自動的に復旧させる方向で試行錯誤しましたが、コンバート対象となるコミュニティをある程度類推することはできますが、その類推を正確なものにするのは困難なため、修復用に対話型のタスクをひとつ作成し、類推できないサイトについても(多少面倒にはなりますが)復旧がおこなえるような形で実装し直そうと思います。
匿名ユーザー さんが13年以上前に更新
- ステータス を Accepted(着手) から Pending Review(レビュー待ち) に変更
- 進捗率 を 0 から 50 に変更
更新履歴 0f226cf5d89eddc5fc609b54528041c2a4ff2780 で適用されました。
Kousuke Ebihara さんが13年以上前に更新
- 題名 を 2系に存在する「中カテゴリ」がそのままカテゴリの一種としてコンバートされる から OpenPNE 2 からのアップグレードで、誤ったコミュニティカテゴリの紐付けをおこなってしまうことがある に変更
Rimpei Ogawa さんが13年以上前に更新
- ステータス を Pending Review(レビュー待ち) から Rejected(差し戻し) に変更
フィードバックします。
- アップグレード後の OpenPNE 3 で OpenPNE 2 からコンバートしたコミュニティを1つ以上削除した状態で、openpne:fix-wrong-categorized-community タスクを実行すると、community_config や community_member の無いコミュニティが作成され、作成されたコミュニティへのアクセスや管理画面のコミュニティ管理のページでエラーが発生します
- community_config や community_member にコンバート時のデータが残っている場合のみコミュニティを作成するようにすればこの問題は回避できるのではないかと思いますが、community_member に関してはメンバー退会時のカスケード削除がなされている可能性があるため、メンバー0人になった場合に処理スキップしたり、管理者の引き継ぎなどの処理を追加で行う必要がありそうです
- アップグレード後にコミュニティカテゴリの紐付けが正しくないと運営者が気づいた場合、コミュニティカテゴリ名を変更したり、コミュニティカテゴリを削除して運用している可能性が考えられますが、このような場合に openpne:fix-wrong-categorized-community タスクを自動実行すると表示上間違ったコミュニティカテゴリに紐付いてしまったり、なくなってしまったコミュニティが修復されないことになります
- おそらくこのような場合には自動修復ではなく、手動修復(ないしは修復なしで運用継続)を選ぶべきだと思いますが、タスクを実行する人に具体的にどのような場合に自動修復を行うべきかの説明をタスク実行時の説明文もしくはタスクを実行する人が目を通すであろうドキュメントに記載してあると分かりやすくなるのではないかと思います
- 「Auto-detected community which OpenPNE 3 recogized to fix are "%d" to "%d" 」の説明文について、
- community が複数形になっていません( are が続くので不自然です)
- recogized の綴りが間違っています
- 末尾の . がこの文だけありません
- 表示される数値がコミュニティIDであることを明記すると分かりやすくなると思いました
Kousuke Ebihara さんが13年以上前に更新
- ステータス を Rejected(差し戻し) から Pending Review(レビュー待ち) に変更
3f9b43dca66f270c874392584b8fd8628cf43408, 6e7de531cbcca07f3b43e95d4368f4362ce760af, 968a0d2fea86e7f0449d43c7512b90ee5d09f9e0 にて http://redmine.openpne.jp/issues/1527#note-13 の指摘点について修正しました。
OpenPNE 2 からインポートされたが OpenPNE 3 で運用をはじめて以降に削除されたコミュニティを誤って復旧させてしまっていた問題の対策として、「community_member にレコードが存在しているかどうか」を確認するようにしました。正しくインポートできたコミュニティを削除する際、 community の外部キー制約によって community_member のレコードは削除されます。一方で、アップグレード時に community にレコードが追加されなかったコミュニティの場合、 community_member にはレコードが存在しています。そこで、「community にレコードはないが community_member にはレコードが存在している」コミュニティを「OpenPNE 2 からのアップグレード時に欠損したコミュニティ」であると判断するようにしました。
また、 note-13 のコメントに従い、いくつかの文章の表現を訂正したり、説明を追記したりしました。
なお、 note-13 のコメントのなかにメンバー退会時(強制退会も含む)の管理権限の交代について記述されていましたが、管理権限についての配慮は特に必要ないのではと考えています。メンバー退会時におこなわれるコミュニティ管理者交代関連の処理は community_member テーブルから取得した ID をベースにおこなわれるため、 community テーブルのレコードの有無は関係しません(なお、この処理は OpenPNE 2 からのバージョンアップがサポートされた 3.4.0 から変更されていません)。community テーブルにレコードが存在しないことによって、メンバーが一人もいなくなった場合にそのコミュニティ自体を削除しようとして失敗する問題は存在しますが、このタスクの挙動に影響があるものではありません。
Rimpei Ogawa さんが13年以上前に更新
- ステータス を Pending Review(レビュー待ち) から Rejected(差し戻し) に変更
note-14 の修正方針については問題ないと思います。
修正内容について、1点フィードバックします。
6e7de531cbcca07f3b43e95d4368f4362ce760af で修正している detectLackedCommunityIdsByCategoryId() 内のクエリについて、
SELECT c_commu_id FROM c_commu WHERE c_commu_category_id = ? AND c_commu_id NOT IN (SELECT id FROM community WHERE id <= ?) AND c_commu_id IN (SELECT community_id FROM community_member WHERE community_id = c_commu_id)NOT IN, IN を使っている箇所を NOT EXISTS, EXISTS を使って以下のようなクエリに書き換えると、サブクエリの結果をすべて取得する必要がなくなるため効率よくなると思います。
SELECT c_commu_id FROM c_commu WHERE c_commu_category_id = ? AND NOT EXISTS (SELECT * FROM community WHERE id = c_commu_id) AND EXISTS (SELECT * FROM community_member WHERE community_id = c_commu_id)合わせて
$oldMaxId
が埋め込まれる id <= ?
の部分を削除していますが、修正前のクエリで NOT IN のサブクエリの結果セットを減らすための条件であったのであれば修正後は不要だと考えて削除しています。Rimpei Ogawa さんが13年以上前に更新
- ステータス を Rejected(差し戻し) から Accepted(着手) に変更
すいません、 http://redmine.openpne.jp/issues/1527#note-15 のコメント書いてから調べたところ、MySQL で EXISTS が遅いという情報がいくつかあったので一旦レビュー待ち状態に戻します。
Rimpei Ogawa さんが13年以上前に更新
- ステータス を Pending Review(レビュー待ち) から Pending Testing(テスト待ち) に変更
- 進捗率 を 50 から 70 に変更
コミュニティ数 10,000、コミュニティメンバー数 100,000 のデータを作成して検証しましたが、実行時間の差がまったくなかったため note-15 の指摘の修正は必要ないと判断し、レビュー済みにします。
必ず1つの行を返す相関サブクエリに対しては、IN は必ず = よりも遅いです。例えば、次の例を
SELECT * FROM t1 WHERE t1.col_name = (SELECT a FROM t2 WHERE b = some_const);この次のクエリの代わりに利用します。
SELECT * FROM t1 WHERE t1.col_name IN (SELECT a FROM t2 WHERE b = some_const);
http://dev.mysql.com/doc/refman/5.1/ja/optimizing-subqueries.html
を参考にすると、以下のような書き換えは有効かもしれません。
SELECT c_commu_id FROM c_commu WHERE c_commu_category_id = ? AND c_commu_id NOT IN (SELECT id FROM community WHERE id <= ?) AND c_commu_id = (SELECT community_id FROM community_member WHERE community_id = c_commu_id LIMIT 1)ただ、これに関しても今回の対象データにおいては差はほとんどないと考えられるので修正の必要はありません。
参考のため EXPLAIN 結果を貼っておきます。
mysql> EXPLAIN SELECT c_commu_id FROM c_commu WHERE c_commu_category_id = 1 AND c_commu_id NOT IN (SELECT id FROM community WHERE id <= 10000) AND c_commu_id IN (SELECT community_id FROM community_member WHERE community_id = c_commu_id); +----+--------------------+------------------+-----------------+----------------------------------------------------+------------------+---------+-----------------------------+-------+--------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+--------------------+------------------+-----------------+----------------------------------------------------+------------------+---------+-----------------------------+-------+--------------------------+ | 1 | PRIMARY | c_commu | ALL | c_commu_category_id,c_commu_category_id_r_datetime | NULL | NULL | NULL | 10000 | Using where | | 3 | DEPENDENT SUBQUERY | community_member | ref | community_id_idx | community_id_idx | 4 | openpne2.c_commu.c_commu_id | 1 | Using where; Using index | | 2 | DEPENDENT SUBQUERY | community | unique_subquery | PRIMARY | PRIMARY | 4 | func | 1 | Using index; Using where | +----+--------------------+------------------+-----------------+----------------------------------------------------+------------------+---------+-----------------------------+-------+--------------------------+ mysql> EXPLAIN SELECT c_commu_id FROM c_commu WHERE c_commu_category_id = 1 AND NOT EXISTS (SELECT * FROM community WHERE id = c_commu_id) AND EXISTS (SELECT * FROM community_member WHERE community_id = c_commu_id); +----+--------------------+------------------+--------+----------------------------------------------------+------------------+---------+-----------------------------+-------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+--------------------+------------------+--------+----------------------------------------------------+------------------+---------+-----------------------------+-------+-------------+ | 1 | PRIMARY | c_commu | ALL | c_commu_category_id,c_commu_category_id_r_datetime | NULL | NULL | NULL | 10000 | Using where | | 3 | DEPENDENT SUBQUERY | community_member | ref | community_id_idx | community_id_idx | 4 | openpne2.c_commu.c_commu_id | 1 | Using index | | 2 | DEPENDENT SUBQUERY | community | eq_ref | PRIMARY | PRIMARY | 4 | openpne2.c_commu.c_commu_id | 1 | Using index | +----+--------------------+------------------+--------+----------------------------------------------------+------------------+---------+-----------------------------+-------+-------------+ mysql> EXPLAIN SELECT c_commu_id FROM c_commu WHERE c_commu_category_id = 1 AND c_commu_id NOT IN (SELECT id FROM community WHERE id <= 10000) AND c_commu_id = (SELECT community_id FROM community_member WHERE community_id = c_commu_id LIMIT 1); +----+--------------------+------------------+-----------------+----------------------------------------------------+------------------+---------+-----------------------------+-------+--------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+--------------------+------------------+-----------------+----------------------------------------------------+------------------+---------+-----------------------------+-------+--------------------------+ | 1 | PRIMARY | c_commu | ALL | c_commu_category_id,c_commu_category_id_r_datetime | NULL | NULL | NULL | 10000 | Using where | | 3 | DEPENDENT SUBQUERY | community_member | ref | community_id_idx | community_id_idx | 4 | openpne2.c_commu.c_commu_id | 1 | Using index | | 2 | DEPENDENT SUBQUERY | community | unique_subquery | PRIMARY | PRIMARY | 4 | func | 1 | Using index; Using where | +----+--------------------+------------------+-----------------+----------------------------------------------------+------------------+---------+-----------------------------+-------+--------------------------+
Minoru Takai さんが13年以上前に更新
このチケットに紐づいている 7 個目のコミット 322393ec711e097b27bc6a21195ef73b073f092f は stable-3.4.x ブランチに対するコミットです。
Yuma Sakata さんが約13年前に更新
- ステータス を Pending Testing(テスト待ち) から Fixed(完了) に変更
- 進捗率 を 70 から 100 に変更
テストOKです。