Transformasi XSL pada MS SQL tanpa CLR

Kadang-kadang sangat mudah untuk mengirim surat langsung dari dalam database, misalnya, pemberitahuan tentang keberhasilan / kegagalan beberapa tindakan, informasi tentang keadaan sistem, log tindakan pengguna, dll. Ini mungkin tampak seperti kebiadaban, sepeda yang mengerikan, solusi miring yang miring, dll - tetapi bayangkan itu benar.

Isi surat dengan metode ini harus dibentuk dalam teks biasa, dan surat dikirim baik melalui xp_sendmail, atau (lebih fleksibel) melalui objek surat COM (misalnya, CDO.Message), membuat dan mengelola melalui pembungkus SQL untuk bekerja dengan OLE sp_OAxxxx.

Keduanya berfungsi selama Anda memiliki cukup sarana ekspresif untuk painttext, hierarki data Anda mendekati nol, dan laporan tersebut dikonsumsi secara eksklusif oleh ijazah teknis sekolah lama, yang

+-----------+--------------+--------------+
| | | |
| | | <EOT> |
+-----------+--------------+--------------+

Apa yang harus dilakukan jika format serupa mulai tegang, dan mendaftarkan komponen Anda di server, atau "muncul" dari tingkat basis data ke tingkat aplikasi untuk mengirim sesuatu yang lebih indah, saya benar-benar tidak ingin:

Seringkali, surat-surat masih perlu diformat (katakanlah, jika sepotong tabel audit dikirim), dan dikirim dalam HTML, setidaknya dalam desain minimal (perusahaan) - dan di sini rutin yang tidak menyenangkan sudah dimulai - sangat membosankan untuk membuat HTML baris-per-baris menggunakan T-SQL, terutama jika ada beberapa jenis laporan surat, struktur laporan bersifat hierarkis, dan desainnya bersifat umum, dan CSS di dalamnya menyebar.

Dalam dunia tingkat aplikasi, dipol XML / XSL yang luar biasa ditemukan 20 tahun yang lalu, sangat koheren sehingga deskripsi sintaksis dari komponen terakhir hampir tidak diingat untuk waktu antara membaca contoh dari MSDN dan menulis elemen template sendiri.

Baiklah, pada akhirnya - pada akhirnya, informasi hierarkis juga jauh lebih nyaman untuk disajikan dalam XML, dan MS SQL secara alami melakukan ini dengan sangat baik selama 15 tahun terakhir.

Jadi, surat-surat yang indah hanya berjarak sepelemparan batu - yang tersisa adalah menemukan cara untuk menambahkan transformasi XSL ke motor ini.

Sayangnya, di luar kotak, MS SQL tidak dapat melakukan ini, dan karena itu sepeda kami akan mengandung roda OLE lain.

Setelah mencari-cari di memori dan di Internet, kami ingat bahwa kami memiliki Microsoft.XMLDOM, yang dapat melintasi landak dan ular dengan memanggil metode transformNode, dan tampaknya tugas kami adalah menginstal semuanya dengan benar, mengambil hasilnya, memproses kemungkinan kesalahan, dan memperbaiki semuanya untuk jangan mengalir memori.

Kami berada di jalur yang benar, dan sp_OACreate, sp_OAMethod, sp_OAGet / SetProperty dan sp_OADestroy akan membantu kami, tetapi ada satu detail kecil - jika metode mengembalikan pointer, maka semuanya baik-baik saja, tetapi jika skalar semuanya buruk, karena skalar string tidak boleh lebih lama dari konstanta fundamental 8kB. Dengan batasan ini, contoh kita akan tetap mendidik selamanya - karena dokumen yang dihasilkan dari 4 (8) ribu karakter di abad kita adalah tawa, dan itu saja.

Kami melewati labirin yang panjang, dan satu langkah menjauh dari pintu keluar, sudah melihat cahaya, tetapi bams! - pintu keluar ditutup oleh kisi. Tidak ada cara untuk mendapatkan string yang lebih panjang dari 8K dari bungkus OLE. Mencoba untuk menentukan tipe (n) varchar (MAX) sebagai parameter penerima mengarah ke beberapa kesalahan OLE yang tidak terdengar.

Kami memahami bahwa bagaimanapun kami membutuhkan sesuatu yang mengembalikan bukan skalar, tetapi sebuah pointer ke hasilnya (karena pointer ke SQL hanyalah angka). Tetapi untuk memaksa metode transformNode untuk mengembalikan pointer tidak mungkin. Setelah putus asa, kami naik ke MSDN (atau apa pun yang online), dan kami melihat bahwa XMLDOM telah berkembang selama 15 tahun terakhir - MS sekarang menawarkan metode transformNodeToObject - BINGO !!! Metode ini mengambil pointer ke aliran yang menuangkan isi dokumen!

Maka itu sederhana - kami membuat objek stream, meneruskannya ke metode, menyuruhnya untuk menuangkan dokumen ke dalam stream.

Akibatnya - dalam aliran bersama kami - dokumen. Bagaimana cara menariknya menjadi skalar? Juga mudah untuk meraup laut dengan lingkaran dalam potongan tidak melebihi 8KB dan merekatkan hasil dalam potongan menjadi varchar (MAX).

Jangan lupa bahwa sebelum Anda mengurangi semuanya, Anda perlu a) membaca ukurannya saat ini dari stream - ini akan menjadi jumlah karakter yang perlu kita dapatkan darinya, dan b) meletakkan pointer untuk membaca stream di awal. Kalau tidak, tidak akan ada yang dibaca.

Bagaimanapun, Anda perlu membersihkan diri sendiri setelahnya. Sangat diharapkan bahwa segala sesuatu yang diciptakan dihancurkan dan apakah itu kesalahan atau tidak, dan jika itu, maka fase apa yang terjadi.

Inilah hasilnya:

CREATE FUNCTION [dbo].[f_Helper_XSLTransform]
(
	@XMLData	XML,
	@XSLTemplate	XML
)
RETURNS NVARCHAR(MAX)
AS
BEGIN

	IF @XMLData IS NULL OR @XSLTemplate IS NULL
		RETURN 'XML or XSL data is NULL'
		 
	DECLARE	@OLEActionName	VARCHAR(128),
		@PropOrMethodName	VARCHAR(128)

	DECLARE @hr				INT, 
		@dummy			INT,
		@ObjXML			INT, 
		@ObjXSL			INT,
		@ObjStream		INT,
		@BuffSize			INT

	DECLARE @Result			NVARCHAR(MAX) = N'',
		@Chunk			NVARCHAR(4000),		-- VVVV should match VVVV
		@ChunkCharSize	INT = 4000,			-- ^^^^^^^^^^^^^^^^^^^^^^
		@ResultBuff		NVARCHAR(MAX) = N''	-- chunk concat buffer

	-- create XMLDOM object for @XMLData
	SET @OLEActionName 	= 'sp_OACreate'
	SET @PropOrMethodName 	= 'Microsoft.XMLDOM'
	
	EXEC @hr = sp_OACreate @PropOrMethodName, @ObjXML OUT
	IF @hr <> 0 
	GOTO FUN_ERROR

	-- create XMLDOM object for @XSLData
	EXEC @hr = sp_OACreate @PropOrMethodName, @ObjXSL OUT
	IF @hr <> 0 
		GOTO FUN_ERROR

	-- create ADODB.Stream object as transformation buffer
	SET @PropOrMethodName 	= 'ADODB.Stream'

	EXEC @hr = sp_OACreate @PropOrMethodName, @ObjStream OUT
	IF @hr <> 0 
		GOTO FUN_ERROR

	-- load XML data
	SET @OLEActionName 	= 'sp_OAMethod'
	SET @PropOrMethodName 	= 'LoadXML'

	EXEC @hr = sp_OAMethod @ObjXML, @PropOrMethodName, @dummy OUT, @XMLData
	IF @hr <> 0 
		GOTO FUN_ERROR

	-- load XSL data
	EXEC @hr = sp_OAMethod @ObjXSL, @PropOrMethodName, @dummy OUT, @XSLTemplate
	IF @hr <> 0 
		GOTO FUN_ERROR

	-- open the stream
	SET @PropOrMethodName 	= 'Open'
	
	EXEC @hr = sp_OAMethod @ObjStream, @PropOrMethodName, NULL
	IF @hr <> 0 
		GOTO FUN_ERROR

	-- trying to do XSL transformation, using the stream as the receiver to work around 4/8K limitation
	SET @PropOrMethodName 	= 'TransformNodeToObject'

	EXEC @hr = sp_OAMethod @ObjXML, @PropOrMethodName, NULL, @ObjXSL, @ObjStream
	IF @hr <> 0 
		GOTO FUN_ERROR

	-- get the size of the stream to read back
	SET @OLEActionName 	= 'sp_OAGetProperty'
	SET @PropOrMethodName 	= 'Size'
	
	EXEC @hr = sp_OAGetProperty @ObjStream, @PropOrMethodName, @BuffSize OUT
	IF @hr <> 0 
		GOTO FUN_ERROR

	-- re-position the stream to the head..
	SET @OLEActionName 	= 'sp_OASetProperty'
	SET @PropOrMethodName 	= 'Position'

	EXEC @hr = sp_OASetProperty @ObjStream, 'Position', 0	-- offset = 0
	IF @hr <> 0 
		GOTO FUN_ERROR

	-- ..and then read chunk by chunk
	SET @OLEActionName 	= 'sp_OAMethod'
	SET @PropOrMethodName 	= 'ReadText'

	WHILE @BuffSize > @ChunkCharSize
	BEGIN
		-- read chunk
		EXEC @hr = sp_OAMethod @ObjStream, @PropOrMethodName, @Chunk OUT, @ChunkCharSize
		IF @hr <> 0 
			GOTO FUN_ERROR
	
		-- append it to the accumulated buffer contents..
		SET @ResultBuff += @Chunk
		-- ..and correct the char counter by the # of chars retrieved
		SET @BuffSize -= @ChunkCharSize
	END

	-- read the last chunk
	EXEC @hr = sp_OAMethod @ObjStream, @PropOrMethodName, @Chunk OUT
	IF @hr <> 0 
		GOTO FUN_ERROR

	-- append the last chunk to the buffer
	SET @ResultBuff += @Chunk

	-- close the stream
	SET @PropOrMethodName = 'Close'
	EXEC @hr = sp_OAMethod @ObjStream, @PropOrMethodName, NULL
	IF @hr <> 0 
		GOTO FUN_ERROR

	-- everything is ok, copying buffer to result
	SET @Result = @ResultBuff
	
	-- resulting doc is in @Result, cleaning up and exiting..
	GOTO OLE_RELEASE

FUN_ERROR:

	SET @Result = '#### Error ' + CONVERT(VARCHAR, CONVERT(VARBINARY(4), @hr)) + ', Action ' + @OLEActionName + ', Method/Property ' + @PropOrMethodName + ' ####'
	-- fall through OLE release

OLE_RELEASE:

	-- destroying XML..
	SET @OLEActionName 	= 'sp_OADestroy'
	SET @PropOrMethodName 	= 'Microsoft.XMLDOM'
	IF ISNULL(@ObjXML, 0) <> 0
	BEGIN
		EXEC @hr = sp_OADestroy @ObjXML
		IF @hr <> 0 
			GOTO OLE_RELEASE_ERROR
	END

	-- ..and XSL objects
	IF ISNULL(@ObjXSL, 0) <> 0
	BEGIN	
		EXEC @hr = sp_OADestroy @ObjXSL
		IF @hr <> 0 
			GOTO OLE_RELEASE_ERROR
	END	 

	-- and the stream obj
	SET @PropOrMethodName 	= 'ADODB.Stream'
	IF ISNULL(@ObjStream, 0) <> 0
	BEGIN	
		EXEC @hr = sp_OADestroy @ObjStream
		IF @hr <> 0 
			GOTO OLE_RELEASE_ERROR
	END	 

	-- exiting with returning the resulting document
	RETURN @Result

OLE_RELEASE_ERROR:

	-- OLE release error, exiting with error info
	RETURN '#### Error ' + CONVERT(VARCHAR, CONVERT(VARBINARY(4), @hr)) + ', Action ' + @OLEActionName + ', Method/Property ' + @PropOrMethodName + ' ####'

END

Sekarang periksa seluruh sosis:

Data uji:

declare @xml xml = convert(xml, '<root><item id="1" name="john"/><item id="2" name="bob"/></root>')

Pola primitif:

declare @templatetext varchar(max) = '<?xml version=''1.0'' encoding=''UTF-8''?>
						<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
						<xsl:template match = ''item''>
						<tr><td><xsl:value-of select = ''@id'' /></td><td><xsl:value-of select = ''@name'' /></td></tr>
						</xsl:template>
						<xsl:template match = ''/''>
							<HTML>
							  <BODY>
								<TABLE>
								  <TR><TD colspan=''2''>Item List</TD></TR>
								  <xsl:apply-templates select = ''root'' />
								</TABLE>
							  </BODY>
							</HTML>
						  </xsl:template>
						</xsl:stylesheet>'

Terakhir, kami mengonversi templat dari teks ke XML, dan memanggil fungsi kami:

declare @xsl xml = convert(xml, @templatetext)

select dbo.f_Helper_XSLTransform(@xml, @xsl)

Dan kami mendapatkan output:

<HTML><BODY><TABLE><TR><TD colspan="2">Item List</TD></TR><tr><td>1</td><td>john</td></tr><tr><td>2</td><td>bob</td></tr></TABLE></BODY></HTML>

Tentang cara mengirim semua ini melalui surat (dan bahkan dengan lampiran - dan yang terakhir - dengan hack kotor), saya dapat menulis posting lain, jika menarik

Source: https://habr.com/ru/post/undefined/


All Articles