vB3からDiscourseへの移行完了(旧vB3の「いいね」含む)

Just completed, for the most part (still in test bed, but the “hard parts” are basically done and the migration works fine), migrating a 15 year old vb3 forum, which was originally (believe-it-or-not) a vb2 forum from 2000.

This forum has been around for around 20 years (with unimaginable custom PHP code, tables, plugins, etc), and Discourse is the only forum software, in my view, “worthy” of a migration project, which I started around a week ago, going from feasibility to completion (basically).

First of all, I want to thank the entire Discourse team for such a remarkable “work-of-art” modern forum. Discourse is truly remarkable, in my view (and I am sure many others).

Second, I would like to thank the team who coded the original vbulletin.rb migration script. This script was about 95% OK. and I modified that script to get it to work properly for vb3. For example, vb3 does not have a filedata table like vb4 (related to attachments); so I modified vbulletin.rb to get it to work. Also, there were issues importing child vb3 forums as categories, but I modified the code to import all forums as top level categories and then wrote some postgres psql to create new parent-child categories relationships (or manually configured). There were other “gotchas” (a story for another day) but those goblins made it fun and challenging, but not insurmountable.

Third, I would like to thank Sam and his code lithium.rb where I used his import_likes routine as the basis for my vb3 thank you (from a legacy plugin, not part of vB OOTB) to discourse likes. I made minor changes to the import_likes routine and after a few hours of debugging, got it to work.

As you can see from this 2018 post, migrated today:

I thought it was important for users that their vb thanks migrated to discourse likes and so here is the code I used to migrate the likes for other lost vb3 forum souls:

First of all I created a vb3 mysql table called user_actions and wrote a PHP script to fill it from the legacy vb post_thanks table, like this (not polished, but working):

ubuntu:/var/www/includes/cron# cat thanks_to_discourse.php
 <?php

/**************************************************
+-----------+------------+-----------+-----------+
| thanker   | unixtime   | id        | thanked   |
+-----------+------------+-----------+-----------+
|         1 | 1584149592 | 303045211 | 302093876 |
| 302153369 | 1584136706 | 303045214 | 302116191 |
| 302108573 | 1584128526 | 303045211 | 302093876 |
| 302153369 | 1584126659 | 303042175 | 302116191 |
| 302153369 | 1584126400 | 303045174 | 302116191 |
| 302153369 | 1584117711 | 303045184 |         1 |
|     37898 | 1584108187 | 303045175 |         1 |
| 302181242 | 1584106664 | 303045201 | 302122047 |
| 302181242 | 1584104642 | 303045074 | 302052697 |
| 302025710 | 1584103722 | 303045184 |         1 |
+-----------+------------+-----------+-----------+
 **************************************************/

$query = 'SELECT p.threadid AS threadid,t.userid AS thanker,t.date AS unixtime,t.postid AS postid,p.userid AS thanked from post_thanks as t LEFT JOIN post p ON p.postid = t.postid';

$allthanks = $vbulletin->db->query_read($query);

/****************************************
mysql> describe user_actions;
+-----------------+------------------+------+-----+---------+----------------+
| Field           | Type             | Null | Key | Default | Extra          |
+-----------------+------------------+------+-----+---------+----------------+
| id              | int(11)          | NO   | PRI | NULL    | auto_increment |
| action_type     | int(11) unsigned | NO   |     | NULL    |                |
| user_id         | int(11) unsigned | NO   |     | NULL    |                |
| target_topic_id | int(11) unsigned | YES  |     | NULL    |                |
| target_post_id  | varchar(16)      | YES  |     | NULL    |                |
| target_user_id  | int(11) unsigned | YES  |     | NULL    |                |
| acting_user_id  | int(11) unsigned | YES  |     | NULL    |                |
| created_at      | timestamp        | YES  |     | NULL    |                |
| updated_at      | timestamp        | YES  |     | NULL    |                |
+-----------------+------------------+------+-----+---------+----------------+
9 rows in set (0.00 sec)
INSERT INTO table_name (column1, column2, column3, ...)
VALUES (value1, value2, value3, ...);
 *************************************/

$action_code = 2; // discourse like action == 2
$target_user_id = "";
while ($action = $vbulletin->db->fetch_array($allthanks)) {

    #code to deal with how discourse identifies the first post in a topic in the post_custom_fields table
    $query = 'SELECT firstpostid FROM thread WHERE threadid =' . $action['threadid'] . ' LIMIT 1';
    $threadinfo = $vbulletin->db->query_first($query);
    if ($threadinfo['firstpostid'] == $action['postid']) {
        $action['postid'] = 'thread-' . $action['threadid'];
    }

    $update = 'INSERT INTO user_actions (action_type, user_id, target_topic_id, target_post_id, target_user_id,acting_user_id, created_at, updated_at)' .
        ' VALUES (' . $action_code . ',' .
        '"' . $action['thanked'] . '",' .
        '"' . $action['threadid'] . '",' .
        '"' . $action['postid'] . '",' .
        '"' . $target_user_id . '",' .
        '"' . $action['thanker'] . '",' .
        'FROM_UNIXTIME(' . $action['unixtime'] . '),' .
        'FROM_UNIXTIME(' . $action['unixtime'] . '))';
    $doit = $vbulletin->db->query_write($update);
}

So, this mysql user_actions table is included with the migration dump.

Here is the modified import_likes routine I used to complete the migration on the discourse side:

def import_likes
    puts "\nimporting likes..."

    # created mysql user_actions table in vb3 using PHP script and included that table with migration dump
    sql = "select acting_user_id as user_id, target_post_id as post_id, created_at from user_actions"
    results = mysql_query(sql)
    puts "length() method form : #{results.count}\n\n"

    puts "skip loading unique id map"
    existing_map = {}
    PostCustomField.where(name: 'import_id').pluck(:post_id, :value).each do |post_id, import_id|
      existing_map[import_id] = post_id
      #puts "postcustomfield existing_map post_id: #{post_id} import_id #{import_id}\n"
    end

    puts "loading data into temp table"

    #manually created the temp like_data table so I could check the table after session ends
    #DB.exec("create temp table like_data(user_id integer, post_id integer, created_at timestamp without time zone)")
    
    puts "like_data temp table created"
    PostAction.transaction do
      results.each do |result|

        result["user_id"] = user_id_from_imported_user_id(result["user_id"].to_s)
        result["post_id"] = existing_map[result["post_id"].to_s]

        next unless result["user_id"] && result["post_id"]

        puts "insert like table user_id: #{result["user_id"]} post_id #{result["post_id"]}\n"

        DB.exec("INSERT INTO like_data VALUES (:user_id,:post_id,:created_at)",
          user_id: result["user_id"],
          post_id: result["post_id"],
          created_at: result["created_at"]
        )

      end
    end


    puts "creating missing post actions"
    DB.exec <<~SQL

    INSERT INTO post_actions (post_id, user_id, post_action_type_id, created_at, updated_at)
             SELECT l.post_id, l.user_id, 2, l.created_at, l.created_at FROM like_data l
             LEFT JOIN post_actions a ON a.post_id = l.post_id AND l.user_id = a.user_id AND a.post_action_type_id = 2
             WHERE a.id IS NULL
    SQL

    puts "creating missing user actions"
    DB.exec <<~SQL
    INSERT INTO user_actions (user_id, action_type, target_topic_id, target_post_id, acting_user_id, created_at, updated_at)
             SELECT pa.user_id, 1, p.topic_id, p.id, pa.user_id, pa.created_at, pa.created_at
             FROM post_actions pa
             JOIN posts p ON p.id = pa.post_id
             LEFT JOIN user_actions ua ON action_type = 1 AND ua.target_post_id = pa.post_id AND ua.user_id = pa.user_id

             WHERE ua.id IS NULL AND pa.post_action_type_id = 2
    SQL

    # reverse action
    DB.exec <<~SQL
    INSERT INTO user_actions (user_id, action_type, target_topic_id, target_post_id, acting_user_id, created_at, updated_at)
             SELECT p.user_id, 2, p.topic_id, p.id, pa.user_id, pa.created_at, pa.created_at
             FROM post_actions pa
             JOIN posts p ON p.id = pa.post_id
             LEFT JOIN user_actions ua ON action_type = 2 AND ua.target_post_id = pa.post_id AND
                ua.acting_user_id = pa.user_id AND ua.user_id = p.user_id

             WHERE ua.id IS NULL AND pa.post_action_type_id = 2
    SQL
    puts "updating like counts on posts"

    DB.exec <<~SQL
        UPDATE posts SET like_count = coalesce(cnt,0)
                  FROM (
        SELECT post_id, count(*) cnt
        FROM post_actions
        WHERE post_action_type_id = 2 AND deleted_at IS NULL
        GROUP BY post_id
    ) x
    WHERE posts.like_count <> x.cnt AND posts.id = x.post_id

    SQL

    puts "updating like counts on topics"

    DB.exec <<-SQL
      UPDATE topics SET like_count = coalesce(cnt,0)
      FROM (
        SELECT topic_id, sum(like_count) cnt
        FROM posts
        WHERE deleted_at IS NULL
        GROUP BY topic_id
      ) x
      WHERE topics.like_count <> x.cnt AND topics.id = x.topic_id

    SQL
    end
end

I hope this post helps others who might migrate from vB3 to discourse, since many people used the legacy vb3 “thank you” plugin for many years.

Also, I spent a lot of time in the discourse database and inside a lot of discourse ruby scripts, so this migration was tricky (not for the faint of heart to be sure), but certainly doable.

Actually, I enjoyed doing it. This was the first time in ruby for me; so it was fun to start to learn ruby and as a long time mysql person, the postgres part was mostly routine.

We will be going live soon… Thank You Discourse Team!!!

お疲れ様でした!共有いただき、ありがとうございます!!

@codinghorror ありがとうございます。

小さな問題(グリーmlin)を発見しましたので、これから対応します。import_likes ルーチンでは、トピックの最初の投稿(トピックの開始者による投稿)の「いいね」が移行されていません(返信のみが移行されています)。

袖をまくり上げてこの問題を解決し、更新された import_likes ルーチンを投稿します。

勝利を宣言する前に、もう一つ小さな問題(ゴブリン)を倒さなければならないことが判明しました。少し早すぎましたね。

更新:

vB3 の PHP スクリプトに以下のコードを追加し、vB3 と Discourse がトピック/スレッド内の最初の投稿を扱う方法の違いに対処しました。

    <?php
    #vB3 code to deal with how discourse identifies the first post in a topic in the post_custom_fields table
    $query = 'SELECT firstpostid FROM thread WHERE threadid =' . $action['threadid'] . ' LIMIT 1';
    $threadinfo = $vbulletin->db->query_first($query);
    if ($threadinfo['firstpostid'] == $action['postid']) {
        $action['postid'] = 'thread-' . $action['threadid'];
    }

これで、意図通りに動作し、vB3 での「ありがとう」がトピックの最初の投稿を含む Discourse へ移行されます。これは、移行テーブルを vB3 から Discourse のテーブル post_custom_fields に合わせることで実現されました。

最終テストを行っていますが、予備テストの結果から、正常に動作する見込みです。

また、元の投稿のコードを更新し、上記の PHP フラグメントを追加しました。

パスワードのマイグレーションを行っていないのは、ハッシュが異なるためだと読みました。私のパスワード移行プラグインをご覧ください(こちら)。おそらく既に VB3 ハッシュに対応していると思いますが、対応していない場合は追加することも可能です。

マイケル、ありがとうございます。

パスワードの移行は行わず、ユーザーにパスワードの変更(より強力で長いパスワードへの変更)を推奨する方針です。

いずれにせよ、ご提案いただき感謝いたします。

一つ小さな問題があり、もしご協力いただければ幸いです。

何らかの理由で、プロファイル表示におけるユーザーの「いいね」の総数が正しく更新されていないようです。それ以外の部分は問題なさそうです。rakeを使って問題の解決を試みましたが、必要な結果が得られていません。

これらのプロファイルの「いいね」カウンターを手動で更新するための良い方法をご存知でしょうか?

どの rake タスクを使用しましたか?そして何が問題でしょうか?すべての数値が大きくずれているのか、それとも一部のユーザーに限られているのか、それとも…?

こんにちは。

まず、(当時)私にとって理にかなっていた唯一の rake タスクを試しました(問題は user_actionspost_actions に関連しているため)。しかし、結果は出ませんでした(実際、振り返ってみれば、その rake が何を exactamente 行っているかを確認すべきでした、LOL):

rake user_actions:rebuild    

正直なところ、import_likes マイグレーションスクリプトで何が間違っているのか、まだ特定できていません。何がどこで間違っているかが分かれば、修正できたはずです(問題を見つけるのに数時間から数日費やしても、問題が分かれば修正は 1 分で済むことがよくあります…これが コーディングの本質 です、LOL)。スナップショットから何度か復元し、import_likes ルーチンをいくつか異なる方法で再実行しました(追加の puts 文などを使用し、テーブルを確認しました)。しかし、結果は決定的ではありませんでした。これは、rubydiscourse 全体における私の 未熟さ がさらに複雑にしています。バックエンドにまだ流暢ではありません。(ただし、begin... rescue ... end の使い方は上達してきました…ハハ、これは rubynube としての新しいデバッグツールの一つになっています)。

私が確認したところ、一時的な postgres like_data テーブル(discourse 側)は正常で、正しい形式で全てのデータを含んでおり、エントリは mysql マイグレーションテーブルと vB サイトの両方と一致しています。

プロセスは、user_actions およびおそらく post_actions の作成段階で破綻しているようです。私のテストでは、これらの重要なテーブルへデータが正しく転送されていないことが示されています。

ご参考までに、vB3 のコアマイグレーションは正常です。この問題は、Discourse の「いいね」に変換しているレガシーな vB3 の「ありがとう」プラグインに関連しています。次のステップとして、時間ができたら今日中に、正常な like_data テーブルから user_actions および post_actions への転送プロセスがどこで破綻しているのかを詳しく調べたいと考えています。

昨日のテストに基づいた私の予備的な結論は、ruby raking は役立たないということです。なぜなら、破綻は user_actions または post_actions テーブルの更新プロセスにあるからです。したがって、どこでなぜ破綻しているのかを正確に特定する必要があります。単純な DB タイプの不一致や、コード内の他の些細な問題(グリムリン)かもしれません。

私たちの Discourse サイトはまだ公開されていませんので、緊急事態 ではありません。ここや他の場所の多くの人々同様、移民手続きや銀行業務などのタスクを処理することは、言うまでもなく かなり面倒 になっており、今日も Discourse 以外の用事で市街地へ出かける必要があります。できれば Discourse を探索し、コードのグリムリン を探したいところです。

マイグレーションスクリプトを再度実行しました(スクリプトの変更は行わず)、カスタムの vb3 の thanks から Discourse の likes への移行(import_likes)スクリプトを含めましたが、現在は問題ないようです。

最新の vB データベースダンプから大きな「D」を同期して、どうなるか試してみます!

Discourse に非常に満足しています。この「芸術作品」のようなモダンなフォーラムを提供していただき、改めてありがとうございます。まだテスト中ですが、まもなく公開予定です。

5.6k 件のいいねが寄せられました…それはたくさんの愛ですね :heart:

すべての vb thanksdiscourse likes に移行したところ、2000 年(20 年前)のトピック投稿の移行例を以下に示します。トピック内で移行された「いいね」が表示されています:

…そして、かっこいいプラグインの作成を学ぶのが楽しみになってきました… :slight_smile:

Discourse とそのチームには非常に満足しています… 特に Ruby の begin .... rescue ...puts "look at me".... end は本当に助かりました!

本当です… old dogs も骨を投げてやれば新しい芸を覚えることができます :slight_smile:

TEAM Discourse の皆様、ありがとうございます! 20 年前、vB2 を使って Unix や Linux ユーザー向けのフォーラムを立ち上げ、約 15 年前に vB3 へ「アップグレード」しました。今日は世界中の Unix や Linux ユーザーにとって大きな変化の日です。私たちにとっても、Discourse への移行が正式に開始された記念すべき日です。

このソフトウェアを開発し、オープンソースプロジェクトとして誰もが利用できるようにしてくださり、心から感謝申し上げます。皆様のご厚意に深く感謝しています。私の見解では(そして多くの皆様も同感だと思いますが)、2020 年現在、Discourse は地球上で最も優れたフォーラムソフトウェアです。

一方、vB3 のレガシーユーザーで Discourse への移行を検討されている方々へ申し上げますと、私の説明ほど移行は「簡単」ではありません。プログラミングに精通し、コマンドラインからデータベースと直接連携することに慣れていない限り、meta.discourse.org にいる才能あふれるプロフェッショナルな方々のサービスを利用されることをお勧めします。

改めて、TEAM Discourse の皆様に感謝申し上げます!

備考:新しいサイトにて、今回の移行で得た教訓をいくつか追加予定です。