Visio VBAを掌握する極限の知見:SQL ServerメタデータとVSDXの双方向同期アーキテクチャ
こんにちは。エンタープライズ領域の自動化インフラを統括しているチーフアーキテクトだ。
日々の業務で、大量のVisio図面(.vsdx)と、SQL Serverなどの外部データベースの間を行き来するメタデータの管理に疲弊していないだろうか。
「図面を修正したが、DB側の進捗ステータスや設計バージョンが古いままになっている」
「誰がどのリビジョンを最終版として保存したのか、ファイルプロパティを見ても分からない」
この手の課題に対し、場当たり的なマクロや、手動のコピペで凌ぎ切ろうとする現場を数多く見てきた。しかし、組織の規模が拡大するにつれて、その非効率な運用は必ず破綻する。
今回は、Visioの`Document`イベントをフックし、SQL Server上のメタデータとVSDXのファイルプロパティを完全同期させる、実戦投入可能な双方向連携アーキテクチャを授けよう。
—
なぜ「その場しのぎのVBA」は現場で破綻するのか?
多くの開発者がやりがちな失敗は、ボタンクリック時に都度ADO(ActiveX Data Objects)接続を開き、SQLを発行するだけの安易な実装だ。このアプローチには、以下の致命的な欠陥がある。
1. イベントの非同期性と無限ループの罠
プロパティを書き換える行為自体が、図面の「変更(Dirty)」フラグを立てる。これを制御せずにイベント内で保存やプロパティ操作を行うと、イベントが無限に連鎖し、VBAの実行時エラーまたはVisio自体のクラッシュを引き起こす。
2. コネクションのリークとトランザクション管理の欠如
VBAのライフサイクルとデータベースのコネクション切断が曖昧だと、ネットワーク切断時にVisioプロセスごとハングアップする。
3. ユーザービリティの無視
保存ボタンを押してからDB通信が完了するまで画面がフリーズし、ユーザーにストレスを与える。
これらをクリアし、エンタープライズ環境に耐えうる堅牢性を手に入れるには、「イベントの適切なライフサイクル管理」「ADOのパラメータ化クエリによるインジェクション対策」「エラーハンドリングの徹底」が不可欠となる。
—
アーキテクチャ概要
今回の実装では、以下のコンポーネントを連携させる。
- Visio `Document` クラスモジュール (`EventMonitor`): `DocumentSaved` などのイベントをキャッチする。
- データベースアクセスクラス (`DataAccessor`): ADODBを用いたSQL Serverとの堅牢な通信をカプセル化する。
- カスタムドキュメントプロパティ: VSDX内にメタデータ(ProjectID, Revision, Status等)を保持する領域。
—
プロダクションコード実装
以下のコードは、そのままあなたのVisioマクロ有効テンプレート(.vstm)や図面ファイルに組み込めるレベルにまで昇華させた実用コードだ。
1. データベース接続・同期ロジック (`DataAccessor` 標準モジュール)
まずはSQL Serverとの通信と、プロパティへの読み書きを担当するコアロジックを記述する。
Option Explicit
‘ =========================================================================
‘ 模块名: DataAccessor
‘ 概要: SQL ServerとのADO接続およびVisioドキュメントプロパティの同期制御
‘ =========================================================================
Private Const CONNECTION_TIMEOUT As Long = 15
Private Const COMMAND_TIMEOUT As Long = 30
‘ 接続文字列の構築(環境に合わせて変更してください)
Private Function GetConnectionString() As String
GetConnectionString = “Provider=MSOLEDBSQL;” & _
“Server=sqlserver.example.com\instance01;” & _
“Database=EngineeringDB;” & _
“Trusted_Connection=yes;” & _
“DataTypeCompatibility=80;”
End Function
Public Sub PullMetadataFromDB(ByVal doc As Visio.Document, ByVal projectId As String)
Dim conn As Object
Dim cmd As Object
Dim rs As Object
Set conn = CreateObject(“ADODB.Connection”)
conn.ConnectionTimeout = CONNECTION_TIMEOUT
conn.Open GetConnectionString
Set cmd = CreateObject(“ADODB.Command”)
Set cmd.ActiveConnection = conn
cmd.CommandText = “SELECT Revision, Status, LastUpdatedBy FROM dbo.ProjectMetadata WHERE ProjectID = ?”
cmd.CommandType = 1 ‘ adCmdText
‘ SQLインジェクションを防ぐためのパラメータ化クエリ
cmd.Parameters.Append cmd.CreateParameter(“pID”, 200, 1, 50, projectId) ‘ adVarChar
Set rs = cmd.Execute
If Not (rs.BOF And rs.EOF) Then
‘ データベースの最新情報をVisioのカスタムプロパティへ書き込む
Call SetCustomProperty(doc, “DB_Revision”, rs.Fields(“Revision”).Value)
Call SetCustomProperty(doc, “DB_Status”, rs.Fields(“Status”).Value)
Call SetCustomProperty(doc, “DB_LastSync”, Now)
Else
Err.Raise vbObjectError + 1000, “PullMetadataFromDB”, “指定されたProjectID (” & projectId & “) がDBに存在しません。”
End If
rs.Close
conn.Close
Set rs = Nothing
Set cmd = Nothing
Set conn = Nothing
End Sub
Public Sub PushMetadataToDB(ByVal doc As Visio.Document, ByVal projectId As String)
Dim conn As Object
Dim cmd As Object
Set conn = CreateObject(“ADODB.Connection”)
conn.ConnectionTimeout = CONNECTION_TIMEOUT
conn.Open GetConnectionString
Set cmd = CreateObject(“ADODB.Command”)
Set cmd.ActiveConnection = conn
‘ アップサート(存在すれば更新、なければ挿入)または更新クエリ
cmd.CommandText = “UPDATE dbo.ProjectMetadata SET Revision = ?, Status = ?, LastUpdatedBy = ?, UpdatedAt = SYSDATETIME() WHERE ProjectID = ?”
cmd.CommandType = 1
Dim rev As String, status As String, user As String
rev = GetCustomProperty(doc, “DB_Revision”)
status = GetCustomProperty(doc, “DB_Status”)
user = Environ(“USERNAME”)
cmd.Parameters.Append cmd.CreateParameter(“pRev”, 200, 1, 20, rev)
cmd.Parameters.Append cmd.CreateParameter(“pStatus”, 200, 1, 50, status)
cmd.Parameters.Append cmd.CreateParameter(“pUser”, 200, 1, 50, user)
cmd.Parameters.Append cmd.CreateParameter(“pID”, 200, 1, 50, projectId)
cmd.Execute
conn.Close
Set cmd = Nothing
Set conn = Nothing
End Sub
‘ — ヘルパー関数:カスタムプロパティの取得・設定 —
Private Sub SetCustomProperty(ByVal doc As Visio.Document, ByVal propName As String, ByVal propValue As Variant)
Dim propRow As Integer
On Error Resume Next
propRow = doc.DocumentSheet.CellsRowIndex(“Prop.” & propName)
If Err.Number <> 0 Then
‘ プロパティが存在しない場合は行を追加
doc.DocumentSheet.AddRow visSectionProp, visRowLast, 0
doc.DocumentSheet.Cells(“Prop.” & propName).Name = propName
Err.Clear
End If
On Error GoTo 0
doc.DocumentSheet.Cells(“Prop.” & propName & “.Value”).FormulaU = “””” & CStr(propValue) & “”””
End Sub
Private Function GetCustomProperty(ByVal doc As Visio.Document, ByVal propName As String) As String
On Error GoTo ErrorHandler
GetCustomProperty = doc.DocumentSheet.Cells(“Prop.” & propName & “.Value”).ResultStr(visNoTranslate)
Exit Function
ErrorHandler:
GetCustomProperty = “”
End Function
2. イベントフックとライフサイクル管理 (`ThisDocument` コードモジュール)
Visioのイベントを安全にキャッチするため、`ThisDocument`モジュールにイベントハンドラを記述する。ここでは保存時のイベント(`DocumentSaved`)をトリガーとしてDBにプッシュする。
Option Explicit
‘ =========================================================================
‘ 模块名: ThisDocument
‘ 概要: ドキュメントイベントのフックと二重発火防止制御
‘ =========================================================================
Private m_IsSyncing As Boolean
Private Sub Document_DocumentSaved(ByVal doc As Visio.Document)
‘ 既に同期処理中の場合は再入を防ぐ(無限ループ防止)
If m_IsSyncing Then Exit Sub
On Error GoTo ErrorHandler
m_IsSyncing = True
‘ ステータスバーに進捗を表示
Application.StatusBar = “SQL Serverへ図面メタデータを同期中…”
Dim projectId As String
projectId = GetCustomPropertyInternal(doc, “ProjectID”)
If projectId <> “” Then
‘ データベースへ現在のプロパティ状態を送信
PushMetadataToDB doc, projectId
Application.StatusBar = “SQL Serverへのメタデータ同期が完了しました。”
Else
Application.StatusBar = “ProjectIDが未設定のため、DB同期をスキップしました。”
End If
m_IsSyncing = False
Exit Sub
ErrorHandler:
m_IsSyncing = False
Application.StatusBar = “【エラー】DB同期に失敗しました: ” & Err.Description
MsgBox “データベースとの同期中にエラーが発生しました。” & vbCrLf & Err.Description, vbCritical, “同期エラー”
End Sub
Private Function GetCustomPropertyInternal(ByVal doc As Visio.Document, ByVal propName As String) As String
On Error GoTo ErrorHandler
GetCustomPropertyInternal = doc.DocumentSheet.Cells(“Prop.” & propName & “.Value”).ResultStr(visNoTranslate)
Exit Function
ErrorHandler:
GetCustomPropertyInternal = “”
End Function
—
現場で絶対に押さえるべき設計上の注意点
1. `m_IsSyncing` フラグによるリエントラント(再入)防止
プロパティを更新したり、プログラムから保存を行ったりすると、再び `DocumentSaved` イベントが誘発される。フラグによるガードがないとスタックオーバーフローを起こすため、この設計は必須である。
2. ネットワーク切断時のフェイルセーフ
データベースサーバが落ちている状態で保存をブロックしてしまうと、ユーザーは「ファイルすら保存できない」という致命的な状況に陥る。プロダクション環境では、DB接続エラー時はログをローカルに吐き出すか、警告を出した上でローカルの保存自体は阻害しないフォールバック設計にすべきだ。(上記のコードではエラーメッセージを表示して処理を安全に抜けている)
3. 認証情報のセキュアな扱い
今回はTrusted Connection(Windows認証)を使用しているため接続文字列にクレデンシャルを含めずに済んでいるが、SQL Server認証を使う場合はプレーンテキストでコード内にパスワードを書く愚行は避け、Windows資格情報マネージャーや環境変数から動的に取得する設計を取り入れること。
—
チーフアーキテクトからの提言
VBAは、しばしば「おもちゃの言語」と揶揄されることがある。しかしそれは、書く側の設計リテラシーが低い場合の話だ。今回紹介したようなイベントライフサイクルの制御、外部DBとの堅牢なトランザクション連携、エラーハンドリングを網羅していれば、Visioは単なるお絵描きツールから、企業のエンジニアリングデータを統括する強力なフロントエンド端末へと生まれ変わる。
コピペで動かして終わりにするのではなく、なぜこの構造が必要なのかを咀嚼し、君の現場の自動化基盤をワンランク上のレベルへと引き上げてほしい。健闘を祈る。
