vB3에서 Discourse로의 마이그레이션 완료 (레거시 vB3의 감사 인사 포함)

거의 완료되었습니다 (아직 테스트 환경에 있지만, "핵심 부분"은 기본적으로 끝났고 마이그레이션도 잘 작동합니다). 15년 된 vb3 포럼을 마이그레이션한 것입니다. 이 포럼은 (믿기 어렵겠지만) 2000년 vb2 포럼이었습니다.

이 포럼은 약 20년 동안 운영되어 왔습니다 (상상할 수 없는 수준의 커스텀 PHP 코드, 테이블, 플러그인 등). 제 생각에는 마이그레이션 프로젝트를 위해 “값어치 있는” 포럼 소프트웨어는 Discourse뿐이며, 약 1주 전부터 실행 가능성 검토부터 완료(기본적으로)까지 진행했습니다.

첫째, 현대 포럼이라는 놀라운 "작품"을 만들어 주신 Discourse 팀 전체에 감사의 말씀을 드립니다. 제 생각에는 (그리고 많은 다른 분들도 동의할 것입니다) Discourse는 정말로 놀라운 소프트웨어입니다.

둘째, 원래 vbulletin.rb 마이그레이션 스크립트를 작성한 팀에게 감사의 말씀을 드립니다. 이 스크립트는 약 95% 정도 괜찮았으며, vb3에서 제대로 작동하도록 스크립트를 수정했습니다. 예를 들어, vb4에는 첨부 파일과 관련된 filedata 테이블이 있지만 vb3에는 없습니다. 따라서 작동하도록 vbulletin.rb를 수정했습니다. 또한 자식 vb3 포럼을 범주로 가져오는 과정에서 문제가 발생했지만, 코드를 수정하여 모든 포럼을 최상위 범주로 가져온 다음, postgres psql을 사용하여 새로운 부모-자식 카테고리 관계를 생성했습니다 (또는 수동으로 구성했습니다). 다른 "함정"들도 있었지만 (또 다른 이야기의 주제입니다) 그 고블린들은 재미있고 도전적이었지만, 불가능한 수준은 아니었습니다.

셋째, 그의 코드 lithium.rb에서 그의 import_likes 루틴을 vb3 thank you (레거시 플러그인에서 가져온 것으로, vB 기본 기능은 아님)를 discourse likes로 변환하는 기초로 사용한 Sam에게 감사의 말씀을 드립니다. import_likes 루틴에 작은 수정을 가하고 몇 시간의 디버깅 끝에 작동하도록 만들었습니다.

오늘 마이그레이션된 이 2018년 게시물을 보면 알 수 있습니다:

사용자의 vb thanks가 discourse likes로 마이그레이션되는 것이 중요하다고 생각했기에, 다른 lost vb3 forum souls의 likes를 마이그레이션하는 데 사용한 코드를 여기에 공유합니다:

우선 vb3 mysql table인 user_actions를 생성하고, 레거시 vb post_thanks 테이블에서 데이터를 채우기 위해 PHP 스크립트를 작성했습니다. 다음과 같이요 (완벽하지는 않지만 작동합니다):

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

따라서 이 mysql user_actions 테이블은 마이그레이션 덤프에 포함됩니다.

Discourse 쪽에서 마이그레이션을 완료하기 위해 사용한 수정된 import_likes 루틴은 다음과 같습니다:

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

vB3 to discourse로 마이그레이션하려는 다른 분들에게 도움이 되기를 바랍니다. 많은 사람들이 수년 동안 레거시 vb3 “감사” 플러그인을 사용했기 때문입니다.

또한, discourse database와 많은 discourse ruby scripts 안에서 많은 시간을 보냈기 때문에 이 마이그레이션은 까다로웠습니다 (물론 faint of heart인 분들에게는 쉽지 않았을 것입니다) 하지만 분명히 가능합니다.

사실 저는 이 작업을 즐기며 했습니다. 제게는 ruby를 처음 해보는 것이었기 때문에 ruby를 배우기 시작하는 것이 재미있었고, 오랜 mysql person으로서 postgres part는 대부분 일상적인 작업이었습니다.

곧 정식 서비스를 시작할 예정입니다… Discourse 팀 감사합니다!!!

잘하셨네요! 공유해 주셔서 감사합니다!!

@codinghorror 감사합니다.

작은 문제를 하나 발견했는데, 곧 해결할 예정입니다. import_likes 루틴이 "주제 시작자"의 첫 게시물(답글 제외)에 대한 좋아요를 이전하지 않는 문제가 있습니다.

소매를 걷어붙이고 이 문제를 해결한 후, 업데이트된 import_likes 루틴을 다시 공유하겠습니다.

아직 하나 더 작은 문제를 해결해야 승리를 선언할 수 있을 것 같아, 조금 성급하게 게시물을 올린 것 같습니다.

업데이트:

vB3와 Discourse가 주제/스레드의 첫 게시물을 처리하는 방식의 차이를 보상하기 위해 vB3의 PHP 스크립트에 다음 코드를 추가했습니다:

    <?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 해시를 지원할 것이며, 그렇지 않다면 해당 기능을 추가하는 데 열려 있습니다.

Michael, 감사합니다.

우리는 비밀번호를 마이그레이션하지 않을 것이며, 사용자가 비밀번호를 변경(그리고 더 강력하고 긴 비밀번호로 변경)하기를 선호합니다.

도움을 제안해 주셔서 감사합니다. 정말 감사드립니다.

아마도 여러분이 우리를 도와줄 수 있는 작은 문제가 하나 있습니다.

어떤 이유에서인지, 모든 것이 정상적으로 보이는 것 같지만 프로필 뷰에서 총 사용자 좋아요 수가 올바르게 업데이트되지 않고 있습니다. rake로 문제를 해결하려고 시도했지만, 제 rake 작업은 제가 원하는 결과를 내주지 않습니다.

이 프로필 “좋아요” 카운터를 수동으로 업데이트하는 좋은 방법을 아시나요?

어떤 rake 태스크를 사용했나요? 그리고 무엇이 문제인가요? 모든 숫자가 크게 벗어나나요, 아니면 일부 사용자만 그런가요, 아니면…?

안녕하세요.

우선, 제게는 유일하게 의미가 있었던 rake를 시도해 보았습니다(당시에는 문제가 user_actions와 post_actions와 관련되어 있었기 때문이죠). 하지만 결과는 없었습니다(사후에 보니, 그 rake가 정확히 어떤 일을 하는지 먼저 확인해 봤어야 했는데, 아하하):

rake user_actions:rebuild    

솔직히 말하면, 아직까지 import_likes 마이그레이션 스크립트에서 정확히 무엇이 잘못되는지 파악하지 못했습니다. 정확히 무엇이, 어디서 잘못되었는지 알았다면 수정할 수 있었을 텐데(문제를 찾는 데 수 시간에서 수 일씩 걸리지만, 문제를 찾은 후에는 수정은 1분이면 끝나는 경우가 많습니다… 코딩의 본질 아하하). 스냅샷을 복원한 후 import_likes 루틴을 여러 가지 다른 방식으로(추가 puts 문장 등) 다시 실행하고 테이블을 검토해 보았지만, 결과는 불확실했습니다. 이는 제가 ruby와 discourse 전반에 대해 아직 초보자 수준이고, 백엔드에는 능숙하지 않기 때문에 더욱 복잡해졌습니다. (다만, begin... rescue ... end 사용은 점점 익숙해지고 있습니다… 하하, 이는 제 새로운 디버깅 도구가 되었습니다, ruby 초보자로서).

제가 확인한 바로는, 임시 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)로 변환하는 커스텀 스크립트를 포함해서 실행한 결과 지금은 정상적으로 작동하는 것 같습니다.

이제 대문자 "D"를 사용하여 최신 vB DB 덤프를 동기화해 보고 어떤 결과가 나올지 확인해 보겠습니다!

Discourse에 매우 만족하고 있습니다… 이 “작품” 같은 현대적인 포럼을 만들어 주셔서 다시 한번 감사합니다. 아직 테스트 중이지만, 곧 공식 출시할 예정입니다.

좋아요 5.6개… 정말 많은 사랑이네요 :heart:

모든 vb thanks를 discourse likes로 전환한 후, 2000년, 즉 20년 전의 마이그레이션된 주제 게시물 예시를 보여드립니다. 주제 내에서 마이그레이션된 좋아요가 표시되어 있습니다:

… 이제 멋진 플러그인을 작성하는 법을 배우는 것이 기대되기 시작합니다… :slight_smile:

Discourse와 팀에 매우 만족합니다… 특히 ruby의 begin .... rescue ...puts "look at me".... end도요… 정말 구세주 같은 존재입니다!

사실… old dogs에게 뼈를 던져주면 새로운 기술을 배울 수 있다고 하죠 :slight_smile:

TEAM Discourse 여러분, 감사합니다! 20년 전, 우리는 vB2에서 유닉스 및 리눅스 사용자를 위한 포럼을 시작했고, 약 15년 전에는 vB3로 '업그레이드’했습니다. 오늘, 유닉스 및 리눅스 사용자 전 세계와 우리 모두에게 큰 변화가 되었습니다. Discourse로의 마이그레이션이 이제 라이브로 전환되었기 때문입니다.

이 소프트웨어를 개발해 주시고 오픈 소스 프로젝트로 누구나 사용할 수 있게 해 주신 데 대해 진심으로 감사드립니다. 여러분의 관대함은 큰 감사를 받습니다. 제 생각에는(수많은 다른 분들도 동의하실 것입니다) Discourse는 2020년 현재 지구상에서 단연 최고의 포럼 소프트웨어입니다.

Discourse로 마이그레이션을 원하는 기존 vB3 사용자들을 위해 말씀드리자면, 이 마이그레이션은 제가 보여드린 것만큼 “쉬운” 작업이 아닙니다. 프로그래밍에 정통하고 커맨드 라인에서 데이터베이스와 직접 통합하는 작업에 익숙하지 않으시다면, meta.discourse.org의 재능 있고 활발하게 활동하는 전문가들의 서비스를 고려해 보시기를 권장합니다.

다시 한번 감사드립니다, Team Discourse!

참고: 저는 새 사이트의 이 게시글에 다양한 경험을 바탕으로 한 교훈들을 추가할 예정입니다: