تحويل XSL على MS SQL بدون CLR

في بعض الأحيان يكون من السهل جدًا إرسال رسائل مباشرة من داخل قاعدة البيانات ، على سبيل المثال ، إشعارات حول نجاح / فشل بعض الإجراءات ، ومعلومات حول حالة النظام ، وسجلات إجراءات المستخدم ، وما إلى ذلك. قد يبدو الأمر مثل الوحشية ، أو الدراجة الوحشية ، أو الحل المائل المنحرف ، وما إلى ذلك - ولكن فقط تخيل ذلك.

يجب أن يتم تشكيل محتوى الرسالة بهذه الطريقة في نص عادي ، ويتم إرسال البريد إما من خلال xp_sendmail ، أو (بشكل أكثر مرونة) عبر كائن بريد COM (على سبيل المثال ، CDO.Message) ، وإنشاءها وإدارتها من خلال برامج تضمين SQL للعمل مع OLE sp_OAxxxx.

كلاهما يعمل طالما أن لديك وسائل تعبيرية كافية لرسم النص ، فإن التسلسل الهرمي لبياناتك يقترب من الصفر ، ويتم استهلاك التقرير حصريًا من قبل دبلوم فني قديم في المدرسة ، والذي

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

ماذا أفعل إذا بدأ تنسيق مماثل في الضغط ، وتسجيل مكوناتك على الخادم ، أو "الخروج" من مستوى قاعدة البيانات إلى مستوى التطبيق لإرسال شيء أكثر جمالًا ، لا أريد حقًا:

في كثير من الأحيان ، لا تزال هناك حاجة إلى تنسيق الرسائل (على سبيل المثال ، إذا تم إرسال جزء من جدول التدقيق) ، وإرساله بتنسيق HTML ، على الأقل في تصميم (الشركة) على الأقل - وهنا بدأ روتين غير سار بالفعل - من الصعب جدًا إنشاء HTML سطرًا بسطر باستخدام T-SQL ، خاصة إذا كان هناك عدة أنواع مختلفة من تقارير البريد ، فإن بنية التقرير هي هرمية ، والتصميم عام ، وينتشر CSS فيه.

في عالم مستوى التطبيق ، تم اختراع ثنائي القطب XML / XSL المذهل قبل 20 عامًا ، متماسكًا للغاية بحيث بالكاد يتم وضع وصف بناء الجملة الأخير في الاعتبار في الفترة بين قراءة مثال من MSDN وكتابة عنصر من قالبه الخاص.

حسنًا ، حسنًا - في النهاية ، تعتبر المعلومات الهرمية أيضًا أكثر ملاءمة للتمثيل في XML ، ويقوم MS SQL بذلك بشكل جيد جدًا على مدى السنوات الـ 15 الماضية.

لذا ، فإن الحروف الجميلة ليست سوى رمية حجر - كل ما تبقى هو إيجاد طريقة لإضافة تحويل XSL إلى هذه الدراجة.

لسوء الحظ ، خارج الصندوق ، MS SQL غير قادرة على القيام بذلك ، وبالتالي ستحتوي دراجتنا على عجلة OLE أخرى.

بعد تفشي الذاكرة وعلى الإنترنت ، نتذكر أن لدينا Microsoft.XMLDOM ، والتي يمكنها عبور القنفذ والثعبان من خلال استدعاء طريقة convertNode ، ويبدو أن مهمتنا هي تثبيت كل شيء بشكل صحيح ، والتقاط النتيجة ، ومعالجة الأخطاء المحتملة ، وإصلاح كل شيء من أجل لا تتدفق الذاكرة.

نحن على المسار الصحيح ، و sp_OACreate و sp_OAMethod و sp_OAGet / SetProperty و sp_OADestroy ستساعدنا ، ولكن هناك تفاصيل صغيرة واحدة - إذا أعادت الطريقة المؤشر ، فكل شيء على ما يرام ، ولكن إذا كان العدد كله سيئًا ، لأن لا يمكن أن يكون عددي السلسلة أطول من الثابت الأساسي 8 كيلو بايت. مع هذا القيد ، سيظل مثالنا تعليميًا إلى الأبد - حيث أن الوثيقة الناتجة من 4 (8) آلاف حرف في قرننا هي ضحكة ، وهذا كل شيء.

لقد مررنا في متاهة طويلة ، وعلى بعد خطوة من المخرج ، نرى النور بالفعل ، ولكن الخفافيش! - المخرج مغلق بشبكة. لا توجد طريقة للحصول على سلسلة أطول من 8 كيلو من مجمّع OLE. محاولة تحديد نوع (n) varchar (MAX) حيث تؤدي معلمة الاستلام إلى خطأ OLE غير مسموع.

نحن نتفهم أننا بطريقة ما نحتاج إلى شيء لا يُرجع عدديًا ، ولكن مؤشرًا للنتيجة (لأن مؤشر SQL مجرد رقم). ولكن لا يمكن فرض طريقة convertNode لإرجاع المؤشر. بعد اليأس ، نتسلق إلى MSDN (أو أيًا كان على الإنترنت) ، ونرى أن XMLDOM قد تطورت على مدى السنوات الـ 15 الماضية - تقدم MS الآن طريقة convertNodeToObject - BINGO !!! تأخذ الطريقة مؤشرًا إلى الدفق الذي تصب فيه محتويات المستند!

بعد ذلك الأمر بسيط - قم بإنشاء كائن دفق ، وقم بتمريره إلى الطريقة ، واطلب منه صب المستند في الدفق.

ونتيجة لذلك - في الدفق معنا - وثيقة. كيف تسحبه إلى عددي؟ من السهل أيضًا تجريف البحر بدوائر في قطع لا تتجاوز 8 كيلوبايت ولصق النتيجة في القطع إلى varchar (MAX).

لا تنس أنه قبل طرح كل شيء ، تحتاج إلى: أ) قراءة حجمه الحالي من الدفق - سيكون هذا هو عدد الأحرف التي نحتاج إلى الحصول عليها منه ، و ب) وضع المؤشر لقراءة الدفق في البداية. خلاف ذلك ، لن يكون هناك شيء للقراءة.

حسنًا ، بعد كل شيء ، تحتاج إلى تنظيف بعناية بعد نفسك. من المرغوب فيه أن يتم تدمير كل ما تم إنشاؤه وما إذا كان خطأ أم لا ، وإذا حدث ذلك ، فما هي المرحلة التي حدثت.

ها هي النتيجة:

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

تحقق الآن من السجق بالكامل:

بيانات الاختبار:

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

النمط البدائي:

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>'

أخيرًا ، نقوم بتحويل القالب من نص إلى XML ، واستدعاء وظيفتنا:

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

select dbo.f_Helper_XSLTransform(@xml, @xsl)

ونحصل على الإخراج:

<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>

حول كيفية إرسال كل هذا عن طريق البريد (وحتى مع المرفقات - وآخرها - مع اختراق متسخ) ، يمكنني كتابة مشاركة أخرى ، إذا كانت مثيرة للاهتمام

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


All Articles