たまたまAzureのSQL DATABASE(≒SQL Server)のドキュメントを読んでいたときに気が付いたのですが、最近のSQL Serverってクエリ末尾に”FOR JSON”を付与すればクエリ結果をそのままJSONに出来るんですって!?そんなこと知らずにDapperでガリガリとSQLをパースしてJSONを作っていたとか馬鹿丸出しですねワタシ。パトラッシュ・・次から頑張るよ・・・・。
ということでSQLだけで多層ネストJSONが出来るまでを検証しました。以下にその方法を示します。
準備:Azure上にサンプルDBを作成 (テンプレートの”AdventureWorkLT”を利用)
このサンプルデータを利用して「セールスの存在する顧客別にオーダーとそのオーダー詳細を表示する」という3層ネスト構造を再現します。
SQLで表現するとこんな感じです。
SELECT CUST.CustomerID, CUST.FirstName, CUST.LastName, CUST.Phone, SHDR.SalesOrderID, SHDR.OrderDate, SHDR.SubTotal, DETL.SalesOrderDetailID, DETL.ProductID, PRDT.Name, DETL.LineTotal FROM SalesLT.Customer CUST INNER JOIN SalesLT.SalesOrderHeader SHDR ON SHDR.CustomerID = CUST.CustomerID INNER JOIN SalesLT.SalesOrderDetail DETL ON DETL.SalesOrderID = SHDR.SalesOrderID INNER JOIN SalesLT.Product PRDT ON PRDT.ProductID = DETL.ProductID ORDER BY CUST.CustomerID,SHDR.SalesOrderID,DETL.SalesOrderDetailID
実行するとこんな感じ。
いかにもDapperで分割してくださいと言わんばかりの出力結果ですが、今回はDapperは使わずSQL DATABASEの基本機能だけしか使いません。ですのでこのSQLの文末に”FOR JSON AUTO”を付与すればめでたく解決・・・ではなく、ここからネストさせたい階層ごとにサブクエリを定義していく必要があります。それをストアドファンクション化したのが以下となります。
CREATE OR ALTER FUNCTION GetSalesInfoByValidCustomer() RETURNS nvarchar(MAX) AS BEGIN DECLARE @json nvarchar(MAX) SET @json = ( SELECT CUST.CustomerID, CUST.FirstName, CUST.LastName, CUST.Phone, ( SELECT SHDR.SalesOrderID, SHDR.OrderDate, SHDR.SubTotal ,( SELECT DETL.SalesOrderDetailID, DETL.ProductID, PRDT.Name, DETL.LineTotal FROM SalesLT.SalesOrderDetail DETL INNER JOIN SalesLT.Product PRDT ON PRDT.ProductID = DETL.ProductID WHERE DETL.SalesOrderID = SHDR.SalesOrderID ORDER BY DETL.SalesOrderDetailID FOR JSON PATH ) AS DETAIL FROM SalesLT.SalesOrderHeader SHDR WHERE SHDR.CustomerID = CUST.CustomerID ORDER BY SHDR.SalesOrderID FOR JSON PATH ) AS [ORDERS] FROM SalesLT.Customer CUST INNER JOIN SalesLT.SalesOrderHeader SHDR ON SHDR.CustomerID = CUST.CustomerID ORDER BY CUST.CustomerID FOR JSON PATH,ROOT('INFO') ) RETURN @json; END
ポイントは次の通りです。
- JSONの受けは必ずNVARCHAR(MAX)とする
- “FOR JSON PATH”とする(PATH付けないとコケる)
- ROOT(‘INFO’)とすることでルートオブジェクト配列にアクセスしやすくする
- サブクエリでのエイリアスがJSONでの型名となる
こうして作成したJSONをオンラインパーサーで確認するとちゃんと3層ネスト構造となっていました。
あとはこのまま普通にHTTP TriggerやPHPファイルからレスポンスを返してあげればREST APIの作成完了なのです。余談ですが逆にSQLだけでJSONを解析することも可能です。
--JSON⇒テーブル化用クエリ DECLARE @json nvarchar(MAX) SET @json= dbo.GetSalesInfoByValidCustomer(); SELECT Level2.CustomerID, Level2.FirstName, Level2.LastName, Level2.Phone, Level2.SalesOrderID, Level2.OrderDate, Level2.SubTotal, SalesOrderDetailID, ProductID, Name , LineTotal FROM ( SELECT Level1.CustomerID, Level1.FirstName, Level1.LastName, Level1.Phone, SalesOrderID, OrderDate, SubTotal, DETAIL FROM ( SELECT CustomerID, FirstName, LastName, Phone, ORDERS FROM OPENJSON(@json,N'strict $.INFO') WITH ( CustomerID int, FirstName nvarchar(50), LastName nvarchar(50), Phone nvarchar(25), ORDERS nvarchar(MAX) AS JSON ) ) Level1 CROSS APPLY OPENJSON(Level1.ORDERS) WITH( SalesOrderID int, OrderDate datetime, SubTotal money, DETAIL nvarchar(MAX) AS JSON ) ) Level2 CROSS APPLY OPENJSON(Level2.DETAIL) WITH( SalesOrderDetailID int, ProductID int, Name nvarchar(50), LineTotal numeric(38,6) ) ;
実はこの逆変換の方が大変でした。ポイントは次の通りです。
- “CROSS APPLY OPENJSON”を用いてJSON型を手動で展開する
- 展開したJSONの中に再度JSON型があった場合はさらに展開する
- OPENJSONのWITHにて型指定する必要がある(型推論ないんかい・・・
- ”CROSS APPLY”で展開した列名は展開対象のエイリアスとは別となる
- “CROSS APPLY”しても展開元のエイリアスは呼べる(これになかなか気付かなかった・・・)
今どきのSQLServerってこんな感じでSQLを駆使していけばJSON化もパースもできるんですね。でも楽になった気があまりしないのは気のせいではないかもしれません。正直、慣れてなければ面倒くさいからこれならDapper使うわってのもアリではないでしょうか。あとSQLの見通しが悪くなってるのがかなり保守観点でマイナス。
ですので間違ってもこれらの「SQLだけでREST APIは作れる」という知識だけで 判断してはダメです。
「(言語名)経験○○年の要員がホゲホゲ」なんて考えのマトモにスキル判定も出来ない無能揃いのSES業者が「運用でSQLバリバリやってましたHAHAHA」という定型作業しか経験のない人材を押し込んでくることになりかねないので要注意です。