거의 완료되었습니다 (아직 테스트 환경에 있지만, "핵심 부분"은 기본적으로 끝났고 마이그레이션도 잘 작동합니다). 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 팀 감사합니다!!!



