[Phần 1 | Phần 2 | Phần 3]
Trong bài đăng cuối cùng của tôi, tôi đã hướng dẫn cách sử dụng TSqlParser
và TSqlFragmentVisitor
để trích xuất thông tin quan trọng từ tập lệnh T-SQL có chứa các định nghĩa thủ tục được lưu trữ. Với tập lệnh đó, tôi đã bỏ sót một số thứ, chẳng hạn như cách phân tích cú pháp OUTPUT
và READONLY
từ khóa cho các tham số và cách phân tích cú pháp nhiều đối tượng với nhau. Hôm nay, tôi muốn cung cấp một tập lệnh xử lý những điều đó, đề cập đến một số cải tiến khác trong tương lai và chia sẻ kho lưu trữ GitHub mà tôi đã tạo cho công việc này.
Trước đây, tôi đã sử dụng một ví dụ đơn giản như sau:
CREATE PROCEDURE dbo.procedure1 @param1 AS int = /* comment */ -64 AS PRINT 1; GO
Và với mã Khách truy cập mà tôi đã cung cấp, đầu ra cho bảng điều khiển là:
==========================Thủ tục Tham khảo
============================
dbo.procedure1
==========================
Thủ tục tham số
==========================
Tên tham số:@ param1
Loại tham số:int
Mặc định:-64
Bây giờ, điều gì sẽ xảy ra nếu tập lệnh được chuyển vào trông giống như thế này? Nó kết hợp định nghĩa thủ tục khủng khiếp có chủ đích từ trước với một vài yếu tố khác mà bạn có thể gây ra sự cố, như tên kiểu do người dùng xác định, hai dạng khác nhau của OUT
/ OUTPUT
từ khóa, Unicode trong giá trị tham số (và trong tên tham số!), từ khóa dưới dạng hằng số và ký tự thoát ODBC.
/* AS BEGIN , @a int = 7, comments can appear anywhere */ CREATE PROCEDURE dbo.some_procedure -- AS BEGIN, @a int = 7 'blat' AS = /* AS BEGIN, @a int = 7 'blat' AS = -- */ @a AS /* comment here because -- chaos */ int = 5, @b AS varchar(64) = 'AS = /* BEGIN @a, int = 7 */ ''blat''', @c AS int = -- 12 6 AS -- @d int = 72, DECLARE @e int = 5; SET @e = 6; GO CREATE PROCEDURE [dbo].another_procedure ( @p1 AS [int] = /* 1 */ 1, @p2 datetime = getdate OUTPUT,-- comment, @p3 date = {ts '2020-02-01 13:12:49'}, @p4 dbo.tabletype READONLY, @p5 geography OUT, @p6 sysname = N'学中' ) AS SELECT 5
Tập lệnh trước đó không xử lý nhiều đối tượng một cách chính xác và chúng tôi cần thêm một vài yếu tố logic để giải thích cho OUTPUT
và READONLY
. Cụ thể, Output
và ReadOnly
không phải là các loại mã thông báo, mà đúng hơn chúng được nhận dạng như một Identifier
. Vì vậy, chúng tôi cần một số logic bổ sung để tìm số nhận dạng có các tên rõ ràng đó trong bất kỳ ProcedureParameter
nào miếng. Bạn có thể nhận ra một vài thay đổi nhỏ khác:
Add-Type -Path "Microsoft.SqlServer.TransactSql.ScriptDom.dll"; $parser = [Microsoft.SqlServer.TransactSql.ScriptDom.TSql150Parser]($true)::New(); $errors = [System.Collections.Generic.List[Microsoft.SqlServer.TransactSql.ScriptDom.ParseError]]::New(); $procedure = @" /* AS BEGIN , @a int = 7, comments can appear anywhere */ CREATE PROCEDURE dbo.some_procedure -- AS BEGIN, @a int = 7 'blat' AS = /* AS BEGIN, @a int = 7 'blat' AS = -- */ @a AS /* comment here because -- chaos */ int = 5, @b AS varchar(64) = 'AS = /* BEGIN @a, int = 7 */ ''blat''', @c AS int = -- 12 6 AS -- @d int = 72, DECLARE @e int = 5; SET @e = 6; GO CREATE PROCEDURE [dbo].another_procedure ( @p1 AS [int] = /* 1 */ 1, @p2 datetime = getdate OUTPUT,-- comment, @p3 date = {ts '2020-02-01 13:12:49'}, @p4 dbo.tabletype READONLY, @p5 geography OUT, @p6 sysname = N'学中' ) AS SELECT 5 "@ $fragment = $parser.Parse([System.IO.StringReader]::New($procedure), [ref]$errors); $visitor = [Visitor]::New(); $fragment.Accept($visitor); class Visitor: Microsoft.SqlServer.TransactSql.ScriptDom.TSqlFragmentVisitor { [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.TSqlFragment] $fragment) { $fragmentType = $fragment.GetType().Name; if ($fragmentType -in ("ProcedureParameter", "ProcedureReference")) { if ($fragmentType -eq "ProcedureReference") { Write-Host "`n=========================="; Write-Host " $($fragmentType)"; Write-Host "=========================="; } $output = ""; $param = ""; $type = ""; $default = ""; $extra = ""; $isReadOnly = $false; $isOutput = $false; $seenEquals = $false; for ($i = $fragment.FirstTokenIndex; $i -le $fragment.LastTokenIndex; $i++) { $token = $fragment.ScriptTokenStream[$i]; if ($token.TokenType -notin ("MultiLineComment", "SingleLineComment", "As")) { if ($fragmentType -eq "ProcedureParameter") { if ($token.TokenType -eq "Identifier" -and ($token.Text.ToUpper -in ("OUT", "OUTPUT", "READONLY")) { $extra = $token.Text.ToUpper(); if ($extra -eq "READONLY") { $isReadOnly = $true; } else { $isOutput = $true; } } if (!$seenEquals) { if ($token.TokenType -eq "EqualsSign") { $seenEquals = $true; } else { if ($token.TokenType -eq "Variable") { $param += $token.Text; } else { if (!$isOutput -and !$isReadOnly) { $type += $token.Text; } } } } else { if ($token.TokenType -ne "EqualsSign" -and !$isOutput -and !$isReadOnly) { $default += $token.Text; } } } else { $output += $token.Text.Trim(); } } } if ($param.Length -gt 0) { $output = "`nParam name: " + $param.Trim(); } if ($type.Length -gt 0) { $type = "`nParam type: " + $type.Trim(); } if ($default.Length -gt 0) { $default = "`nDefault: " + $default.TrimStart(); } if ($isReadOnly) { $extra = "`nRead Only: yes"; } if ($isOutput) { $extra = "`nOutput: yes"; } Write-Host $output $type $default $extra; } } }
Mã này chỉ dành cho mục đích trình diễn và không có khả năng nó là mã mới nhất. Vui lòng xem chi tiết bên dưới về cách tải xuống phiên bản mới hơn.
Đầu ra trong trường hợp này:
==========================Thủ tục Tham khảo
============================
dbo.some_procedure
Tên tham số
:@a
Loại tham số:int
Mặc định:5
Tên tham số:@b
Loại tham số:varchar (64)
Mặc định:'AS =/ * BEGIN @a, int =7 * / "blank"'
Tên tham số
:@c
Loại tham số:int
Mặc định:6
==========================
Thủ tục tham khảo
============================
[dbo] .aosystem_procedure
Tên tham số
:@ p1
Loại tham số:[int]
Mặc định:1
Tên tham số
:@ p2
Loại tham số:datetime
Mặc định:getdate
Đầu ra:yes
Tên tham số:@ p3
Loại tham số:ngày
Mặc định:{ts '2020-02-01 13:12:49'}
Tên tham số:@ p4
Loại tham số:dbo.tabletype
Chỉ đọc:có
Tên tham số
:@ p5
Loại tham số:địa lý
Đầu ra:có
Tên tham số:@ p6
Loại tham số:sysname
Mặc định:N '学 中'
Đó là một số phân tích cú pháp khá mạnh mẽ, mặc dù có một số trường hợp tẻ nhạt và rất nhiều logic có điều kiện. Tôi muốn xem TSqlFragmentVisitor
được mở rộng để một số loại mã thông báo của nó có các thuộc tính bổ sung (như SchemaObjectName.IsFirstAppearance
và ProcedureParameter.DefaultValue
) và xem các loại mã thông báo mới được thêm vào (như FunctionReference
). Nhưng ngay cả bây giờ, đây là năm ánh sáng vượt quá trình phân tích cú pháp bạo lực mà bạn có thể viết trong bất kỳ ngôn ngữ, đừng bận tâm đến T-SQL.
Tuy nhiên, vẫn còn một số hạn chế mà tôi chưa giải quyết:
- Điều này chỉ giải quyết các thủ tục được lưu trữ. Mã để xử lý tất cả ba loại chức năng do người dùng xác định là tương tự , nhưng không có
FunctionReference
tiện dụng loại phân mảnh, vì vậy thay vào đó bạn cần xác địnhSchemaObjectName
đầu tiên phân mảnh (hoặc bộIdentifier
đầu tiên vàDot
mã thông báo) và bỏ qua bất kỳ trường hợp nào tiếp theo. Hiện tại, mã trong bài đăng này sẽ trả lại tất cả thông tin về tham số vào một chức năng, nhưng nó sẽ không trả về tên của hàm . Hãy thoải mái sử dụng nó cho các tệp đơn hoặc lô chỉ chứa các thủ tục được lưu trữ, nhưng bạn có thể thấy đầu ra khó hiểu đối với nhiều loại đối tượng hỗn hợp. Phiên bản mới nhất trong kho lưu trữ bên dưới xử lý các chức năng hoàn toàn tốt. - Mã này không lưu trạng thái. Việc xuất ra bảng điều khiển trong mỗi Lượt truy cập rất dễ dàng, nhưng việc thu thập dữ liệu từ nhiều lượt truy cập, sau đó chuyển sang nơi khác, phức tạp hơn một chút, chủ yếu là do cách thức hoạt động của mẫu Khách truy cập.
- Mã trên không thể chấp nhận đầu vào trực tiếp. Để đơn giản hóa việc trình diễn ở đây, đó chỉ là một tập lệnh thô nơi bạn dán khối T-SQL của mình dưới dạng một hằng số. Mục tiêu cuối cùng là hỗ trợ đầu vào từ một tệp, một mảng tệp, một thư mục, một mảng thư mục hoặc kéo các định nghĩa mô-đun từ cơ sở dữ liệu. Và đầu ra có thể ở bất cứ đâu:tới bảng điều khiển, tệp, cơ sở dữ liệu… vì vậy bầu trời là giới hạn ở đó. Một số công việc đó đã xảy ra trong thời gian chờ đợi, nhưng không có công việc nào trong số đó được viết trong phiên bản đơn giản mà bạn thấy ở trên.
- Không có cách xử lý lỗi nào. Một lần nữa, để ngắn gọn và dễ tiêu thụ, mã ở đây không lo lắng về việc xử lý các trường hợp ngoại lệ không thể tránh khỏi, mặc dù điều phá hoại nhất có thể xảy ra ở dạng hiện tại là một lô sẽ không xuất hiện trong đầu ra nếu nó không thể đúng cách. được phân tích cú pháp (như
CREATE STUPID PROCEDURE dbo.whatever
). Khi chúng tôi bắt đầu sử dụng cơ sở dữ liệu và / hoặc hệ thống tệp, việc xử lý lỗi thích hợp sẽ trở nên quan trọng hơn nhiều.
Bạn có thể tự hỏi, tôi sẽ ở đâu để duy trì công việc đang diễn ra và khắc phục tất cả những điều này? Chà, tôi đã đưa nó lên GitHub, tạm gọi dự án là ParamParser và đã có những người đóng góp giúp cải tiến. Phiên bản hiện tại của mã trông khá khác so với mẫu ở trên và vào thời điểm bạn đọc nó, một số hạn chế được đề cập ở đây có thể đã được giải quyết. Tôi chỉ muốn duy trì mã ở một nơi; mẹo này thiên về hiển thị một mẫu tối thiểu về cách nó có thể hoạt động và nhấn mạnh rằng có một dự án dành riêng để đơn giản hóa nhiệm vụ này.
Trong phân đoạn tiếp theo, tôi sẽ nói thêm về cách người bạn và đồng nghiệp của tôi, Will White, đã giúp tôi chuyển từ tập lệnh độc lập mà bạn thấy ở trên sang mô-đun mạnh mẽ hơn nhiều mà bạn sẽ tìm thấy trên GitHub.
Nếu bạn có nhu cầu phân tích cú pháp các giá trị mặc định từ các tham số trong thời gian chờ đợi, vui lòng tải xuống mã và dùng thử. Và như tôi đã đề xuất trước đây, hãy tự mình thử nghiệm, bởi vì có rất nhiều điều mạnh mẽ khác mà bạn có thể làm với các lớp này và kiểu Khách truy cập.
[Phần 1 | Phần 2 | Phần 3]