0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プリザンターのマークダウンエディタからリッチテキストエディタにSQLで一括移行する方法

0
Posted at

はじめに

プリザンターの説明項目(Description)や内容(Body)では、入力列スタイルとして「マークダウン」と「リッチテキストエディタ」を選択できます。マークダウンは記法を覚える必要がありますが、フィルタやソートとの相性が良い一方、リッチテキストエディタは直感的な WYSIWYG 編集ができるのが魅力です。

運用を進めていく中で「やっぱりリッチテキストエディタに変更したい」という場面が出てくることがあります。テーブルの管理画面から1つずつ設定を変えれば済む話ですが、対象テーブルが多い場合は SQL で一括変更したいところです。

この記事では、サイト設定の現状を SQL で調査し、SiteSetting JSON と保存済みデータを書き換えてマークダウンからリッチテキストエディタへ一括移行する方法を紹介します。SQL はすべて各データベースの組込み JSON 関数を使い、安全に処理します。

SQL でデータベースを直接操作する方法です。実行前に必ずバックアップを取得してください。また、テスト環境で検証してから本番に適用することを強くお勧めします。

バージョン 1.4.6.0 以降を対象にしています

マークダウンとリッチテキストの違いを整理する

まず、マークダウンエディタとリッチテキストエディタの違いを整理しておきます。

項目 マークダウン リッチテキスト
入力方式 テキストベース(記法を覚える必要あり) WYSIWYG(所見即所得)
データ形式 プレーンテキスト([md] プレフィックス付き) HTML
フィルタ・ソート 対応 テキストとして扱われる(HTML タグ込み)
CSV エクスポート テキストそのまま HTML タグ込み
画像挿入 対応 対応
表の作成 Markdown テーブル記法 ツールバーから挿入

SiteSetting での設定値

プリザンターの説明項目の入力列スタイルは、Sites テーブルの SiteSettings カラムに格納されている JSON の中で、各カラムの FieldCss プロパティとして管理されています。

入力列スタイル FieldCss の値
ノーマル null(プロパティなし)
ワイド "field-wide"
マークダウン "control-markdown"
リッチテキストエディタ "control-markup"

データの保存形式

マークダウンエディタを使っている場合、データの先頭に [md] プレフィックスが付与されます。これはプリザンターがクライアントサイドで Markdown レンダリングを起動するためのフラグです。

[md]
# 見出し
これは **太字** のテキストです。

- リスト1
- リスト2

リッチテキストエディタの場合は、HTML 形式でデータが保存されます。

<h1>見出し</h1>
<p>これは <strong>太字</strong> のテキストです。</p>
<ul>
<li>リスト1</li>
<li>リスト2</li>
</ul>

ステップ1:SiteSetting から現状を調査する

まずは対象環境でマークダウンエディタを使用しているサイトと項目を特定します。各データベースの JSON 関数を使って Columns 配列から FieldCss が "control-markdown" になっているカラムを抽出します。

SQL Server

OPENJSON で Columns 配列を展開し、WITH 句で型指定して抽出します。

SELECT
    s.[SiteId],
    s.[Title],
    col.[ColumnName],
    col.[FieldCss]
FROM
    [Sites] s
    CROSS APPLY OPENJSON(s.[SiteSettings], '$.Columns')
    WITH (
        [ColumnName] NVARCHAR(100) '$.ColumnName',
        [FieldCss]   NVARCHAR(100) '$.FieldCss'
    ) col
WHERE
    col.[FieldCss] = 'control-markdown'
ORDER BY
    s.[SiteId],
    col.[ColumnName];

PostgreSQL

jsonb_array_elements で Columns 配列を展開し、FieldCss の値でフィルタします。

SELECT
    s."SiteId",
    s."Title",
    col->>'ColumnName' AS "ColumnName",
    col->>'FieldCss' AS "FieldCss"
FROM
    "Sites" s,
    jsonb_array_elements(s."SiteSettings"::jsonb -> 'Columns') AS col
WHERE
    col ->> 'FieldCss' = 'control-markdown'
ORDER BY
    s."SiteId",
    col->>'ColumnName';

MySQL

JSON_TABLE で Columns 配列を行に展開します。

SELECT
    s.`SiteId`,
    s.`Title`,
    col.`ColumnName`,
    col.`FieldCss`
FROM
    `Sites` s,
    JSON_TABLE(
        s.`SiteSettings`,
        '$.Columns[*]' COLUMNS (
            `ColumnName` VARCHAR(100) PATH '$.ColumnName',
            `FieldCss`   VARCHAR(100) PATH '$.FieldCss'
        )
    ) col
WHERE
    col.`FieldCss` = 'control-markdown'
ORDER BY
    s.`SiteId`,
    col.`ColumnName`;

これで対象のサイトとカラムの一覧が得られます。一括で変更する前に、ここで得られた結果を確認して対象範囲を絞り込みましょう。

ステップ2:SiteSetting を書き換える

対象が確認できたら、SiteSettings JSON の FieldCss を "control-markdown" から "control-markup" に変更します。各データベースの JSON 操作関数を使い、配列内の対象要素だけを安全に書き換えます。

SQL Server(JSON_MODIFY + OPENJSON)

OPENJSON で配列インデックス(key)を特定し、JSON_MODIFY で書き換えます。

全カラム一括

UPDATE s
SET s.[SiteSettings] = JSON_MODIFY(
    s.[SiteSettings],
    '$.Columns[' + col.[key] + '].FieldCss',
    'control-markup'
)
FROM [Sites] s
CROSS APPLY (
    SELECT TOP 1 [key]
    FROM OPENJSON(s.[SiteSettings], '$.Columns')
    WHERE JSON_VALUE([value], '$.FieldCss') = 'control-markdown'
) col;

Columns 配列に FieldCss が "control-markdown" の要素が複数ある場合は、このクエリを対象がなくなるまで繰り返し実行してください。TOP 1 で1要素ずつ処理するため、ループ実行で安全に全件を変換できます。

特定カラムのみ

UPDATE s
SET s.[SiteSettings] = JSON_MODIFY(
    s.[SiteSettings],
    '$.Columns[' + col.[key] + '].FieldCss',
    'control-markup'
)
FROM [Sites] s
CROSS APPLY (
    SELECT TOP 1 [key]
    FROM OPENJSON(s.[SiteSettings], '$.Columns')
    WHERE JSON_VALUE([value], '$.ColumnName') = 'DescriptionA'
      AND JSON_VALUE([value], '$.FieldCss') = 'control-markdown'
) col;

PostgreSQL(jsonb_set)

jsonb_array_elements で配列インデックスを特定し、jsonb_set でピンポイントに書き換えます。

全カラム一括

UPDATE "Sites" s
SET "SiteSettings" = (
    SELECT jsonb_set(
        s."SiteSettings"::jsonb,
        ARRAY['Columns', (sub.idx)::text, 'FieldCss'],
        '"control-markup"'
    )::text
    FROM (
        SELECT (ordinality - 1) AS idx
        FROM jsonb_array_elements(s."SiteSettings"::jsonb -> 'Columns')
            WITH ORDINALITY
        WHERE value ->> 'FieldCss' = 'control-markdown'
        LIMIT 1
    ) sub
)
WHERE s."SiteSettings"::jsonb -> 'Columns' @>
    '[{"FieldCss":"control-markdown"}]';

Columns 配列に FieldCss が "control-markdown" の要素が複数ある場合は、このクエリを対象がなくなるまで繰り返し実行してください。LIMIT 1 で1要素ずつ処理するため、ループ実行で安全に全件を変換できます。

特定カラムのみ

UPDATE "Sites" s
SET "SiteSettings" = (
    SELECT jsonb_set(
        s."SiteSettings"::jsonb,
        ARRAY['Columns', (sub.idx)::text, 'FieldCss'],
        '"control-markup"'
    )::text
    FROM (
        SELECT (ordinality - 1) AS idx
        FROM jsonb_array_elements(s."SiteSettings"::jsonb -> 'Columns')
            WITH ORDINALITY
        WHERE value ->> 'ColumnName' = 'DescriptionA'
          AND value ->> 'FieldCss' = 'control-markdown'
        LIMIT 1
    ) sub
)
WHERE s."SiteSettings"::jsonb -> 'Columns' @>
    '[{"ColumnName":"DescriptionA","FieldCss":"control-markdown"}]';

MySQL(JSON_SET + JSON_SEARCH)

JSON_SEARCH と JSON_SET を組み合わせて書き換えます。JSON_SEARCH で "control-markdown" の JSON パスを取得し、JSON_SET でそのパスの値を "control-markup" に変更します。

全カラム一括

UPDATE `Sites`
SET `SiteSettings` = JSON_SET(
    `SiteSettings`,
    JSON_UNQUOTE(
        JSON_SEARCH(`SiteSettings`, 'one', 'control-markdown', NULL, '$.Columns[*].FieldCss')
    ),
    'control-markup'
)
WHERE JSON_SEARCH(`SiteSettings`, 'one', 'control-markdown', NULL, '$.Columns[*].FieldCss')
    IS NOT NULL;

JSON_SEARCH は最初にマッチした1件のパスを返します。対象が複数ある場合はこのクエリを繰り返し実行してください。

特定カラムのみ

特定のカラムだけを変更する場合は、JSON_TABLE で配列インデックスを特定してから JSON_SET で書き換えます。

UPDATE `Sites` s
INNER JOIN (
    SELECT
        s2.`SiteId`,
        col.`idx`
    FROM
        `Sites` s2,
        JSON_TABLE(
            s2.`SiteSettings`,
            '$.Columns[*]' COLUMNS (
                `idx`        FOR ORDINALITY,
                `ColumnName` VARCHAR(100) PATH '$.ColumnName',
                `FieldCss`   VARCHAR(100) PATH '$.FieldCss'
            )
        ) col
    WHERE
        col.`ColumnName` = 'DescriptionA'
        AND col.`FieldCss` = 'control-markdown'
) t ON s.`SiteId` = t.`SiteId`
SET s.`SiteSettings` = JSON_SET(
    s.`SiteSettings`,
    CONCAT('$.Columns[', t.`idx` - 1, '].FieldCss'),
    'control-markup'
);

ステップ3:保存済みデータを変換する

SiteSetting を変更しただけでは、既存のレコードに保存されている Markdown テキスト([md] プレフィックス付き)がそのまま残ります。リッチテキストエディタで正しく表示するには、データの変換が必要です。

変換方針

データ変換の方針は大きく2つあります。

方針 メリット デメリット
[md] プレフィックスだけ除去する SQL だけで完結。シンプル Markdown 記法がプレーンテキストとして残る
Markdown → HTML に変換する リッチテキストエディタで正しく表示される 外部ツールが必要

ステップ3ではまず [md] プレフィックスの除去を行います。Markdown → HTML への本格的な変換が必要な場合は、ステップ4で紹介する C# スクリプト(Markdig)や PowerShell での変換を検討してください。

対象カラム

マークダウンエディタが設定できるカラムは Description(説明)と Body(内容)の2種類です。それぞれのデータは以下のテーブルに保存されています。

カラム 保存先テーブル カラム名パターン
Description(説明) Issues / Results DescriptionA 〜 DescriptionZ
Body(内容) Issues / Results Body

Enterprise Edition の項目拡張を使っている環境では、Description001 〜 のカラムも対象になります。
以降の SQL は環境に合わせてカラム名を追加してください。

[md] の後に改行(\n)が1つ入っているため、除去する文字数は5文字([md]\n)です。ただし、[md] の直後に改行なしでテキストが続いている場合もあるので、実際のデータを確認してから実行してください。

Description カラムの [md] プレフィックスを除去する

対象テーブルのデータから [md] プレフィックスを除去します。ここでも対象サイトの特定にはステップ1と同様に JSON 関数を使います。

SQL Server

-- Issues テーブルの DescriptionA から [md] プレフィックスを除去
UPDATE i
SET i.[DescriptionA] = SUBSTRING(i.[DescriptionA], 6, LEN(i.[DescriptionA]))
FROM [Issues] i
WHERE i.[SiteId] IN (
    SELECT s.[SiteId]
    FROM [Sites] s
    CROSS APPLY OPENJSON(s.[SiteSettings], '$.Columns')
    WITH (
        [ColumnName] NVARCHAR(100) '$.ColumnName',
        [FieldCss]   NVARCHAR(100) '$.FieldCss'
    ) col
    WHERE col.[ColumnName] = 'DescriptionA'
      AND col.[FieldCss] = 'control-markup'
)
AND i.[DescriptionA] LIKE '[md]' + CHAR(10) + '%';

PostgreSQL

-- Issues テーブルの DescriptionA から [md] プレフィックスを除去
UPDATE "Issues"
SET "DescriptionA" = SUBSTRING("DescriptionA" FROM 6)
WHERE "SiteId" IN (
    SELECT s."SiteId"
    FROM "Sites" s,
         jsonb_array_elements(s."SiteSettings"::jsonb -> 'Columns') AS col
    WHERE col ->> 'ColumnName' = 'DescriptionA'
      AND col ->> 'FieldCss' = 'control-markup'
)
AND "DescriptionA" LIKE '[md]' || CHR(10) || '%';

MySQL

-- Issues テーブルの DescriptionA から [md] プレフィックスを除去
UPDATE `Issues`
SET `DescriptionA` = SUBSTRING(`DescriptionA`, 6)
WHERE `SiteId` IN (
    SELECT s.`SiteId`
    FROM `Sites` s,
         JSON_TABLE(
             s.`SiteSettings`,
             '$.Columns[*]' COLUMNS (
                 `ColumnName` VARCHAR(100) PATH '$.ColumnName',
                 `FieldCss`   VARCHAR(100) PATH '$.FieldCss'
             )
         ) col
    WHERE col.`ColumnName` = 'DescriptionA'
      AND col.`FieldCss` = 'control-markup'
)
AND `DescriptionA` LIKE CONCAT('[md]', CHAR(10 USING utf8mb4), '%');

DescriptionB 以降のカラムも対象の場合は、クエリ内の 'DescriptionA' をそれぞれのカラム名に変更して実行してください。Results テーブルも同様にテーブル名を置き換えて実行します。

Body カラムの [md] プレフィックスを除去する

Body(内容)カラムもマークダウンを使用している場合は、同様に [md] プレフィックスを除去します。

SQL Server

-- Issues テーブルの Body から [md] プレフィックスを除去
UPDATE i
SET i.[Body] = SUBSTRING(i.[Body], 6, LEN(i.[Body]))
FROM [Issues] i
WHERE i.[SiteId] IN (
    SELECT s.[SiteId]
    FROM [Sites] s
    CROSS APPLY OPENJSON(s.[SiteSettings], '$.Columns')
    WITH (
        [ColumnName] NVARCHAR(100) '$.ColumnName',
        [FieldCss]   NVARCHAR(100) '$.FieldCss'
    ) col
    WHERE col.[ColumnName] = 'Body'
      AND col.[FieldCss] = 'control-markup'
)
AND i.[Body] LIKE '[md]' + CHAR(10) + '%';

PostgreSQL

-- Issues テーブルの Body から [md] プレフィックスを除去
UPDATE "Issues"
SET "Body" = SUBSTRING("Body" FROM 6)
WHERE "SiteId" IN (
    SELECT s."SiteId"
    FROM "Sites" s,
         jsonb_array_elements(s."SiteSettings"::jsonb -> 'Columns') AS col
    WHERE col ->> 'ColumnName' = 'Body'
      AND col ->> 'FieldCss' = 'control-markup'
)
AND "Body" LIKE '[md]' || CHR(10) || '%';

MySQL

-- Issues テーブルの Body から [md] プレフィックスを除去
UPDATE `Issues`
SET `Body` = SUBSTRING(`Body`, 6)
WHERE `SiteId` IN (
    SELECT s.`SiteId`
    FROM `Sites` s,
         JSON_TABLE(
             s.`SiteSettings`,
             '$.Columns[*]' COLUMNS (
                 `ColumnName` VARCHAR(100) PATH '$.ColumnName',
                 `FieldCss`   VARCHAR(100) PATH '$.FieldCss'
             )
         ) col
    WHERE col.`ColumnName` = 'Body'
      AND col.`FieldCss` = 'control-markup'
)
AND `Body` LIKE CONCAT('[md]', CHAR(10 USING utf8mb4), '%');

Results テーブルの Body も同様にテーブル名を置き換えて実行してください。

ステップ4:Markdown → HTML に変換する

ステップ3では [md] プレフィックスを除去しましたが、Markdown 記法のテキストがそのまま残っています。リッチテキストエディタで見出しや太字、リストなどを正しく表示するには、Markdown を HTML に変換する必要があります。

変換には C# スクリプト(Markdig)の使用を推奨します。プリザンター自身が内部で使用しているライブラリのため、最も互換性の高い変換が可能です。PowerShell の ConvertFrom-Markdown でも対応できます。

C# スクリプトで変換する(Markdig)

Markdig は .NET 向けの高性能な Markdown パーサーで、プリザンター自身も内部で使用しているライブラリです。dotnet-script を使えば .csx ファイルとしてスクリプト実行できます。PowerShell の ConvertFrom-Markdown よりも高い互換性で変換できるため、最も確実な方法です。

SQL Server 向け

// Convert-MarkdownToHtml.csx — SQL Server 向け

#r "nuget: Markdig, 0.38.0"
#r "nuget: Microsoft.Data.SqlClient, 5.2.2"

using Markdig;
using Microsoft.Data.SqlClient;

// ---- 設定 ----
var connStr    = "Server=localhost;Database=Implem.Pleasanter;Trusted_Connection=true;TrustServerCertificate=true";
var tableName  = "Issues";       // Issues or Results
var columnName = "DescriptionA"; // DescriptionA, Body, etc.
// --------------

var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
var idCol = tableName == "Issues" ? "IssueId" : "ResultId";

using var conn = new SqlConnection(connStr);
conn.Open();

var selectSql = $@"
SELECT [{idCol}], [{columnName}]
FROM [{tableName}]
WHERE [SiteId] IN (
    SELECT s.[SiteId]
    FROM [Sites] s
    CROSS APPLY OPENJSON(s.[SiteSettings], '$.Columns')
    WITH (
        [ColumnName] NVARCHAR(100) '$.ColumnName',
        [FieldCss]   NVARCHAR(100) '$.FieldCss'
    ) c
    WHERE c.[ColumnName] = '{columnName}'
      AND c.[FieldCss]   = 'control-markup'
)
AND [{columnName}] IS NOT NULL
AND [{columnName}] <> ''";

var rows = new List<(long id, string md)>();
using (var cmd = new SqlCommand(selectSql, conn))
using (var r = cmd.ExecuteReader())
    while (r.Read())
        rows.Add((r.GetInt64(0), r.GetString(1)));

var converted = 0;
foreach (var (id, raw) in rows)
{
    var md = raw;
    if (md.StartsWith("[md]\n")) md = md.Substring(5);
    else if (md.StartsWith("[md]\r\n")) md = md.Substring(6);

    var html = Markdown.ToHtml(md, pipeline);

    using var u = new SqlCommand(
        $"UPDATE [{tableName}] SET [{columnName}]=@h WHERE [{idCol}]=@id", conn);
    u.Parameters.AddWithValue("@h", html);
    u.Parameters.AddWithValue("@id", id);
    u.ExecuteNonQuery();
    converted++;
}

Console.WriteLine($"{converted} 件のレコードを変換しました。");

PostgreSQL 向け

// Convert-MarkdownToHtml.csx — PostgreSQL 向け

#r "nuget: Markdig, 0.38.0"
#r "nuget: Npgsql, 8.0.5"

using Markdig;
using Npgsql;

// ---- 設定 ----
var connStr    = "Host=localhost;Database=Implem.Pleasanter;Username=postgres;Password=yourpassword";
var tableName  = "Issues";       // Issues or Results
var columnName = "DescriptionA"; // DescriptionA, Body, etc.
// --------------

var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
var idCol = tableName == "Issues" ? "IssueId" : "ResultId";

using var conn = new NpgsqlConnection(connStr);
conn.Open();

var selectSql = $@"
SELECT ""{idCol}"", ""{columnName}""
FROM ""{tableName}""
WHERE ""SiteId"" IN (
    SELECT s.""SiteId""
    FROM ""Sites"" s,
         jsonb_array_elements(s.""SiteSettings""::jsonb -> 'Columns') AS col
    WHERE col ->> 'ColumnName' = '{columnName}'
      AND col ->> 'FieldCss'  = 'control-markup'
)
AND ""{columnName}"" IS NOT NULL
AND ""{columnName}"" <> ''";

var rows = new List<(long id, string md)>();
using (var cmd = new NpgsqlCommand(selectSql, conn))
using (var r = cmd.ExecuteReader())
    while (r.Read())
        rows.Add((r.GetInt64(0), r.GetString(1)));

var converted = 0;
foreach (var (id, raw) in rows)
{
    var md = raw;
    if (md.StartsWith("[md]\n")) md = md.Substring(5);
    else if (md.StartsWith("[md]\r\n")) md = md.Substring(6);

    var html = Markdown.ToHtml(md, pipeline);

    using var u = new NpgsqlCommand(
        $@"UPDATE ""{tableName}"" SET ""{columnName}""=@h WHERE ""{idCol}""=@id", conn);
    u.Parameters.AddWithValue("h", html);
    u.Parameters.AddWithValue("id", id);
    u.ExecuteNonQuery();
    converted++;
}

Console.WriteLine($"{converted} 件のレコードを変換しました。");

MySQL 向け

// Convert-MarkdownToHtml.csx — MySQL 向け

#r "nuget: Markdig, 0.38.0"
#r "nuget: MySqlConnector, 2.4.0"

using Markdig;
using MySqlConnector;

// ---- 設定 ----
var connStr    = "Server=localhost;Database=Implem.Pleasanter;User=root;Password=yourpassword";
var tableName  = "Issues";       // Issues or Results
var columnName = "DescriptionA"; // DescriptionA, Body, etc.
// --------------

var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
var idCol = tableName == "Issues" ? "IssueId" : "ResultId";

using var conn = new MySqlConnection(connStr);
conn.Open();

var selectSql = $@"
SELECT {idCol}, {columnName}
FROM {tableName}
WHERE SiteId IN (
    SELECT s.SiteId
    FROM Sites s,
         JSON_TABLE(
             s.SiteSettings,
             '$.Columns[*]' COLUMNS (
                 ColumnName VARCHAR(100) PATH '$.ColumnName',
                 FieldCss   VARCHAR(100) PATH '$.FieldCss'
             )
         ) col
    WHERE col.ColumnName = '{columnName}'
      AND col.FieldCss   = 'control-markup'
)
AND {columnName} IS NOT NULL
AND {columnName} <> ''";

var rows = new List<(long id, string md)>();
using (var cmd = new MySqlCommand(selectSql, conn))
using (var r = cmd.ExecuteReader())
    while (r.Read())
        rows.Add((r.GetInt64(0), r.GetString(1)));

var converted = 0;
foreach (var (id, raw) in rows)
{
    var md = raw;
    if (md.StartsWith("[md]\n")) md = md.Substring(5);
    else if (md.StartsWith("[md]\r\n")) md = md.Substring(6);

    var html = Markdown.ToHtml(md, pipeline);

    using var u = new MySqlCommand(
        $"UPDATE {tableName} SET {columnName}=@h WHERE {idCol}=@id", conn);
    u.Parameters.AddWithValue("@h", html);
    u.Parameters.AddWithValue("@id", id);
    u.ExecuteNonQuery();
    converted++;
}

Console.WriteLine($"{converted} 件のレコードを変換しました。");

C# スクリプトの実行方法

dotnet-script を使って .csx ファイルをスクリプト実行します。

dotnet-script のインストール

# .NET SDK がインストール済みの前提
dotnet tool install -g dotnet-script

スクリプトの実行

スクリプトファイル冒頭の「設定」セクションで接続文字列・テーブル名・カラム名を編集してから実行します。

dotnet script Convert-MarkdownToHtml.csx

Markdig の UseAdvancedExtensions() を指定すると、テーブル記法、タスクリスト、パイプテーブルなど拡張 Markdown 記法もサポートされます。なお、プリザンター本体の Markdown 描画はクライアントサイドの marked(JavaScript)で行われているため、Markdig とはレンダリング結果が完全一致するわけではありません。変換後は実データで表示を確認してください。

PowerShell で変換する

PowerShell 7 以降に搭載されている ConvertFrom-Markdown コマンドレットを使うと、Markdown テキストを HTML に変換できます。データベースに接続して対象レコードを取得し、変換後のデータで更新するスクリプトを紹介します。

SQL Server 向け

# SQL Server: Markdown → HTML 変換スクリプト
# 要件: PowerShell 7以降、SqlServer モジュール

param(
    [string]$ServerInstance = "localhost",
    [string]$Database = "Implem.Pleasanter",
    [string]$TableName = "Issues",        # Issues or Results
    [string]$ColumnName = "DescriptionA"  # DescriptionA, Body, etc.
)

Import-Module SqlServer

# 対象レコードを取得
$query = @"
SELECT
    i.[$($TableName -eq 'Issues' ? 'IssueId' : 'ResultId')] AS [RecordId],
    i.[$ColumnName] AS [MarkdownText]
FROM [$TableName] i
WHERE i.[SiteId] IN (
    SELECT s.[SiteId]
    FROM [Sites] s
    CROSS APPLY OPENJSON(s.[SiteSettings], '$.Columns')
    WITH (
        [ColumnName] NVARCHAR(100) '$.ColumnName',
        [FieldCss]   NVARCHAR(100) '$.FieldCss'
    ) col
    WHERE col.[ColumnName] = '$ColumnName'
      AND col.[FieldCss] = 'control-markup'
)
AND i.[$ColumnName] IS NOT NULL
AND i.[$ColumnName] <> ''
AND i.[$ColumnName] NOT LIKE '<%;%'
"@

$rows = Invoke-Sqlcmd -ServerInstance $ServerInstance -Database $Database -Query $query

$idColumn = if ($TableName -eq 'Issues') { 'IssueId' } else { 'ResultId' }
$converted = 0

foreach ($row in $rows) {
    $md = $row.MarkdownText
    # [md] プレフィックスが残っている場合は除去
    if ($md -match '^\[md\]\r?\n') {
        $md = $md -replace '^\[md\]\r?\n', ''
    }

    # Markdown → HTML 変換
    $html = (ConvertFrom-Markdown -InputObject $md).Html

    # データベースを更新
    $updateQuery = @"
UPDATE [$TableName]
SET [$ColumnName] = @html
WHERE [$idColumn] = @id
"@
    Invoke-Sqlcmd -ServerInstance $ServerInstance -Database $Database `
        -Query $updateQuery `
        -Variable "html=$html", "id=$($row.RecordId)"
    $converted++
}

Write-Host "$converted 件のレコードを変換しました。" -ForegroundColor Green

PostgreSQL 向け

PostgreSQL に接続する場合は Npgsql を使います。

# PostgreSQL: Markdown → HTML 変換スクリプト
# 要件: PowerShell 7以降

param(
    [string]$ConnectionString = "Host=localhost;Database=Implem.Pleasanter;Username=postgres;Password=yourpassword",
    [string]$TableName = "Issues",        # Issues or Results
    [string]$ColumnName = "DescriptionA"  # DescriptionA, Body, etc.
)

# Npgsql をロード(NuGetから取得済みの前提)
Add-Type -Path "path/to/Npgsql.dll"

$idColumn = if ($TableName -eq 'Issues') { 'IssueId' } else { 'ResultId' }

$conn = [Npgsql.NpgsqlConnection]::new($ConnectionString)
$conn.Open()

# 対象レコードを取得
$selectSql = @"
SELECT
    "$idColumn" AS "RecordId",
    "$ColumnName" AS "MarkdownText"
FROM "$TableName"
WHERE "SiteId" IN (
    SELECT s."SiteId"
    FROM "Sites" s,
         jsonb_array_elements(s."SiteSettings"::jsonb -> 'Columns') AS col
    WHERE col ->> 'ColumnName' = '$ColumnName'
      AND col ->> 'FieldCss' = 'control-markup'
)
AND "$ColumnName" IS NOT NULL
AND "$ColumnName" <> ''
AND "$ColumnName" NOT LIKE '<%'
"@

$cmd = $conn.CreateCommand()
$cmd.CommandText = $selectSql
$reader = $cmd.ExecuteReader()

$records = @()
while ($reader.Read()) {
    $records += @{
        RecordId = $reader["RecordId"]
        MarkdownText = $reader["MarkdownText"]
    }
}
$reader.Close()

$converted = 0
foreach ($rec in $records) {
    $md = $rec.MarkdownText
    if ($md -match '^\[md\]\r?\n') {
        $md = $md -replace '^\[md\]\r?\n', ''
    }

    $html = (ConvertFrom-Markdown -InputObject $md).Html

    $updateCmd = $conn.CreateCommand()
    $updateCmd.CommandText = "UPDATE ""$TableName"" SET ""$ColumnName"" = @html WHERE ""$idColumn"" = @id"
    $updateCmd.Parameters.AddWithValue("html", $html) | Out-Null
    $updateCmd.Parameters.AddWithValue("id", $rec.RecordId) | Out-Null
    $updateCmd.ExecuteNonQuery() | Out-Null
    $converted++
}

$conn.Close()
Write-Host "$converted 件のレコードを変換しました。" -ForegroundColor Green

MySQL 向け

MySQL に接続する場合は MySqlConnector を使います。

# MySQL: Markdown → HTML 変換スクリプト
# 要件: PowerShell 7以降、MySqlConnector

param(
    [string]$ConnectionString = "Server=localhost;Database=Implem.Pleasanter;User=root;Password=yourpassword",
    [string]$TableName = "Issues",        # Issues or Results
    [string]$ColumnName = "DescriptionA"  # DescriptionA, Body, etc.
)

# MySqlConnector をロード(NuGetから取得済みの前提)
Add-Type -Path "path/to/MySqlConnector.dll"

$idColumn = if ($TableName -eq 'Issues') { 'IssueId' } else { 'ResultId' }

$conn = [MySqlConnector.MySqlConnection]::new($ConnectionString)
$conn.Open()

# 対象レコードを取得
$selectSql = @"
SELECT
    $idColumn AS RecordId,
    $ColumnName AS MarkdownText
FROM $TableName
WHERE SiteId IN (
    SELECT s.SiteId
    FROM Sites s,
         JSON_TABLE(
             s.SiteSettings,
             '$.Columns[*]' COLUMNS (
                 ColumnName VARCHAR(100) PATH '$.ColumnName',
                 FieldCss   VARCHAR(100) PATH '$.FieldCss'
             )
         ) col
    WHERE col.ColumnName = '$ColumnName'
      AND col.FieldCss = 'control-markup'
)
AND $ColumnName IS NOT NULL
AND $ColumnName <> ''
AND $ColumnName NOT LIKE '<%'
"@

$cmd = [MySqlConnector.MySqlCommand]::new($selectSql, $conn)
$reader = $cmd.ExecuteReader()

$records = @()
while ($reader.Read()) {
    $records += @{
        RecordId = $reader["RecordId"]
        MarkdownText = $reader["MarkdownText"]
    }
}
$reader.Close()

$converted = 0
foreach ($rec in $records) {
    $md = $rec.MarkdownText
    if ($md -match '^\[md\]\r?\n') {
        $md = $md -replace '^\[md\]\r?\n', ''
    }

    $html = (ConvertFrom-Markdown -InputObject $md).Html

    $updateCmd = [MySqlConnector.MySqlCommand]::new(
        "UPDATE $TableName SET $ColumnName = @html WHERE $idColumn = @id", $conn)
    $updateCmd.Parameters.AddWithValue("@html", $html) | Out-Null
    $updateCmd.Parameters.AddWithValue("@id", $rec.RecordId) | Out-Null
    $updateCmd.ExecuteNonQuery() | Out-Null
    $converted++
}

$conn.Close()
Write-Host "$converted 件のレコードを変換しました。" -ForegroundColor Green

使い方

# SQL Server の場合
./Convert-MarkdownToHtml.ps1 -ServerInstance "localhost" -Database "Implem.Pleasanter" `
    -TableName "Issues" -ColumnName "DescriptionA"

# Body カラムも変換
./Convert-MarkdownToHtml.ps1 -ServerInstance "localhost" -Database "Implem.Pleasanter" `
    -TableName "Issues" -ColumnName "Body"

# Results テーブルも変換
./Convert-MarkdownToHtml.ps1 -ServerInstance "localhost" -Database "Implem.Pleasanter" `
    -TableName "Results" -ColumnName "DescriptionA"

# PostgreSQL の場合
./Convert-MarkdownToHtml.ps1 -ConnectionString "Host=localhost;Database=Implem.Pleasanter;Username=postgres;Password=yourpassword" `
    -TableName "Issues" -ColumnName "DescriptionA"

# MySQL の場合
./Convert-MarkdownToHtml.ps1 -ConnectionString "Server=localhost;Database=Implem.Pleasanter;User=root;Password=yourpassword" `
    -TableName "Issues" -ColumnName "DescriptionA"

ConvertFrom-Markdown は PowerShell 7.0 以降で使用できます。PowerShell 5.1(Windows PowerShell)では使用できないため、PowerShell 7 をインストールしてください。

変換後の確認

SQL の実行が完了したら、以下の点を確認しましょう。

SiteSetting の確認

ステップ1の調査クエリを再実行して、FieldCss が "control-markdown" のカラムが残っていないことを確認します。

SQL Server

SELECT COUNT(*)
FROM [Sites] s
CROSS APPLY OPENJSON(s.[SiteSettings], '$.Columns')
WITH (
    [FieldCss] NVARCHAR(100) '$.FieldCss'
) col
WHERE col.[FieldCss] = 'control-markdown';

PostgreSQL

SELECT count(*)
FROM "Sites" s,
     jsonb_array_elements(s."SiteSettings"::jsonb -> 'Columns') AS col
WHERE col ->> 'FieldCss' = 'control-markdown';

MySQL

SELECT COUNT(*)
FROM `Sites` s,
     JSON_TABLE(
         s.`SiteSettings`,
         '$.Columns[*]' COLUMNS (
             `FieldCss` VARCHAR(100) PATH '$.FieldCss'
         )
     ) col
WHERE col.`FieldCss` = 'control-markdown';

データの確認

[md] プレフィックスが残っているレコードがないか確認します。Description と Body の両方を確認しましょう。以下は PostgreSQL の例ですが、SQL Server や MySQL でも同様に LEFT 関数で先頭数文字を確認できます。

-- Description カラムの残存確認
SELECT
    "SiteId",
    "IssueId",
    LEFT("DescriptionA", 20) AS "DescriptionA_preview"
FROM
    "Issues"
WHERE
    LEFT("DescriptionA", 4) = '[md]';
-- Body カラムの残存確認
SELECT
    "SiteId",
    "IssueId",
    LEFT("Body", 20) AS "Body_preview"
FROM
    "Issues"
WHERE
    LEFT("Body", 4) = '[md]';

プリザンターの再起動

SiteSettings はアプリケーション内でキャッシュされています。SQL で直接変更した後は、プリザンターのアプリケーションを再起動してキャッシュを更新してください。IIS の場合はアプリケーションプールのリサイクル、Docker の場合はコンテナの再起動で対応できます。

画面での動作確認

再起動後、対象サイトの編集画面を開いて以下を確認します。

  • 説明項目がリッチテキストエディタで表示されていること
  • 既存データが正しく表示されていること
  • 新規にデータを入力して保存・表示ができること

まとめ

プリザンターのマークダウンエディタからリッチテキストエディタへの SQL 一括移行手順を紹介しました。

  • Sites.SiteSettings JSON 内の FieldCss プロパティが入力列スタイルを制御しており、"control-markdown" がマークダウン、"control-markup" がリッチテキストエディタに対応する
  • ステップ1で JSON 関数を使って対象サイト・カラムを特定し、ステップ2で SiteSetting の FieldCss を書き換え、ステップ3で Description・Body データの [md] プレフィックスを除去する
  • SiteSetting の書き換えには JSON_MODIFY(SQL Server)、jsonb_set(PostgreSQL)、JSON_SET(MySQL)を使い、JSON 構造を壊さず安全に処理する
  • 対象サイトの特定にも OPENJSON(SQL Server)、jsonb_array_elements(PostgreSQL)、JSON_TABLE(MySQL)を使い、文字列マッチングに頼らない正確な抽出を行う
  • Markdown → HTML への本格的な変換が必要な場合は、C# スクリプト(Markdig)を推奨。PowerShell の ConvertFrom-Markdown でも対応可能
  • SQL 実行後はプリザンターの再起動が必要(SiteSetting キャッシュの更新のため)
  • 実行前のバックアップと、テスト環境での事前検証を忘れずに
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?