T-SQL存储过程 - 将筛选参数作为对象/CLR/XML/UDT

9
概述:是否有一种已知模式,可以将标准的“筛选器”类型传递给存储过程来封装startdate/enddate/pagesize/pagenum参数?
我正在探索将筛选对象参数传递给存储过程的想法,该参数封装了我们的常见筛选参数(startdate、enddate、pagenumber、pagesize、int列表等)。这样做的原因是为了减少存储过程中相似参数和样板SQL的数量。这将为我们提供一个更标准的接口和每个过程的起点。我还没有找到有关此主题的详细信息。
我注意到的模式是,在构建大多数SP时,它们都从一个用于where子句的单个id参数开始。稍后,您可能需要添加日期范围参数(startdate、enddate或动态范围“ytd、mtd、dtd”)的参数。如果数据集足够大,您也可能需要引入服务器端分页的pagesize/pagenum。经过一段时间,您可能会意识到需要对一些id进行列表而不是单个id进行结果,因此您将添加CSV或XML参数以包含ID。
最终,许多存储过程都具有类似的样板和(希望)相同的处理这些标准筛选参数的参数。我正在尝试研究传递封装的过滤器对象参数的已知模式,这在C#方面理想情况下应该是强类型。当管理一组驱动所有需要相同筛选选项(除了特定于报告的查询参数)的报告的过程时,这将特别有用。
我的目标是将所需参数的数量减少到WHERE子句所需的最低限度,并创建一种标准机制,以将通用筛选选项传递给过程并在过程内使用这些值。通过XML、CLR或UDT参数如何实现?
对于这个问题的背景,我使用SQL Server 2008通过C# 2.0的ADO.Net。不幸的是,在此项目中,LINQ/EF不是一个选项,我们必须坚持我们现有的RDBMS。如果有一种已知的模式需要更改技术,我会很感兴趣听到它。

我考虑过的一个潜在解决方案是将一个xml序列化的C#对象通过一个UDF反序列化成为一个datatable。但是,在每个过程中仍然需要通过键名来获取相应的值,这还是很麻烦的。最终,我只想对这个概念进行更多的研究 - 它是否在其他平台上可用?是否有一个过滤层/ api,使得在SQL Server上更容易实现?提前感谢。 - mellodev
我使用了很多动态SQL。在这种情况下,问题是通过传递WHERE子句来解决的。 - Gordon Linoff
感谢您的输入。我们已经在使用许多动态SQL,实际上我们为此项目存储的所有SQL都存储在应用程序中,我们在其中附加SqlDataParameters并根据需要进行自己专有的WHERE子句注入。然而,每个SQL块最终都会有很多重复的参数和逻辑,以满足通常的过滤需求,我正在尝试找到一个将它们封装成单个参数的模式。 - mellodev
5个回答

5
我个人认为你在思考过多或试图简化不需要简化的东西。你最好将存储过程参数保持原样,或尝试创建一些基类和帮助函数,可以将一组参数附加到命令对象上。
不过,话虽如此,我还是会提供一个解决方案给你,看看是否符合你的需求:
我建议使用TSQL用户定义类型。创建一个或多个类型。也许一个用于日期范围,一个用于分页和排序。我使用类似的方法将多行数据传递给存储过程。(一些代码可能需要稍作修改,因为我只是修改了一些我已经编写过的代码,而且我已经很长时间没有与DataTable字段一起工作了。)
最终,所有这些只是缩短应用程序方法和匹配存储过程中参数列表的长度。存储过程将负责在表变量中提取或连接信息。下面列出的类确实提供了在.NET应用程序端强类型参数的能力。
if not exists (select * from INFORMATION_SCHEMA.DOMAINS where DOMAIN_SCHEMA = 'dbo' and DOMAIN_NAME = 'DateRange' and DATA_TYPE = 'table type')
begin

    create type dbo.DateRange as table 
    (
        StartDate datetime2 null
        ,EndDate datetime2 null
    )

end
go


if not exists (select * from INFORMATION_SCHEMA.DOMAINS where DOMAIN_SCHEMA = 'dbo' and DOMAIN_NAME = 'Paging' and DATA_TYPE = 'table type')
begin

    create type dbo.Paging as table 
    (
        PageNumber int null
        ,PageSize int null
        ,SortField sysname null
        ,SortDirection varchar(4) null
    )

end
go

SQL用户定义类型可以在.NET应用程序中表示为强类型对象。从一个基类开始:
```

SQL用户定义类型可以在.NET应用程序中表示为强类型对象。从一个基类开始:

```
    Imports System
    Imports System.Data
    Imports System.Data.SqlClient
    Imports System.Runtime.Serialization


    Namespace SqlTypes

        <Serializable()> _
        <System.ComponentModel.DesignerCategory("Code")> _
        Public MustInherit Class SqlTableTypeBase
            Inherits DataTable

            Public Sub New()

                MyBase.New()
                Initialize()

            End Sub


            Public Sub New(ByVal tableName As String)

                MyBase.New(tableName)
                Initialize()

            End Sub


            Public Sub New(ByVal tableName As String, ByVal tableNamespace As String)

                MyBase.New(tableName, tableNamespace)
                Initialize()

            End Sub


            Protected Sub New(ByVal info As SerializationInfo, ByVal context As StreamingContext)

                MyBase.New(info, context)

            End Sub


            ''' <summary>
            ''' Implement this method to create the columns in the data table to match the SQL server user defined table type
            ''' </summary>
            ''' <remarks></remarks>
            Protected MustOverride Sub Initialize()


            Public Function CreateParameter(parameterName As String) As SqlParameter

                Dim p As New SqlParameter(parameterName, SqlDbType.Structured)
                p.Value = Me

                Return p

            End Function

        End Class

    End Namespace

为SQL类型创建一个实现:

Imports System
Imports System.Data
Imports System.Runtime.Serialization


Namespace SqlTypes

    <Serializable()> _
    <System.ComponentModel.DesignerCategory("Code")> _
    Public Class DateRange
        Inherits SqlTableTypeBase

        Public Sub New()

            MyBase.New()

        End Sub


        Public Sub New(ByVal tableName As String)

            MyBase.New(tableName)

        End Sub


        Public Sub New(ByVal tableName As String, ByVal tableNamespace As String)

            MyBase.New(tableName, tableNamespace)

        End Sub


        Protected Sub New(ByVal info As SerializationInfo, ByVal context As StreamingContext)

            MyBase.New(info, context)

        End Sub


        'TODO: throw some more overloaded constructors in here...

        Public Sub New(startDate As DateTime?, endDate As DateTime?)

            MyBase.New()

            Me.StartDate = startDate
            Me.EndDate = endDate

        End Sub


        Public Property StartDate As DateTime?
            Get
                Return CType(Me.Rows(0)(0), DateTime?)
            End Get
            Set(value As DateTime?)
                Me.Rows(0)(0) = value
            End Set
        End Property


        Public Property EndDate As DateTime?
            Get
                Return CType(Me.Rows(0)(1), DateTime?)
            End Get
            Set(value As DateTime?)
                Me.Rows(0)(1) = value
            End Set
        End Property


        Protected Overrides Sub Initialize()

            Me.Columns.Add(New DataColumn("StartDate", GetType(DateTime?)))
            Me.Columns.Add(New DataColumn("EndDate", GetType(DateTime?)))

            Me.Rows.Add({Nothing, Nothing})

        End Sub

    End Class

End Namespace

并且:

Imports System
Imports System.Data
Imports System.Runtime.Serialization


Namespace SqlTypes

    <Serializable()> _
    <System.ComponentModel.DesignerCategory("Code")> _
    Public Class Paging
        Inherits SqlTableTypeBase

        Public Sub New()

            MyBase.New()

        End Sub


        Public Sub New(ByVal tableName As String)

            MyBase.New(tableName)

        End Sub


        Public Sub New(ByVal tableName As String, ByVal tableNamespace As String)

            MyBase.New(tableName, tableNamespace)

        End Sub


        Protected Sub New(ByVal info As SerializationInfo, ByVal context As StreamingContext)

            MyBase.New(info, context)

        End Sub


        'TODO: throw some more overloaded constructors in here...


        Public Sub New(pageNumber As Integer?, pageSize As Integer?)

            MyBase.New()

            Me.PageNumber = pageNumber
            Me.PageSize = pageSize

        End Sub


        Public Sub New(sortField As String, sortDirection As String)

            MyBase.New()

            Me.SortField = sortField
            Me.SortDirection = sortDirection

        End Sub


        Public Sub New(pageNumber As Integer?, pageSize As Integer?, sortField As String, sortDirection As String)

            Me.New(pageNumber, pageSize)

            Me.SortField = sortField
            Me.SortDirection = sortDirection

        End Sub


        Public Property PageNumber As Integer?
            Get
                Return CType(Me.Rows(0)(0), Integer?)
            End Get
            Set(value As Integer?)
                Me.Rows(0)(0) = value
            End Set
        End Property


        Public Property PageSize As Integer?
            Get
                Return CType(Me.Rows(0)(1), Integer?)
            End Get
            Set(value As Integer?)
                Me.Rows(0)(1) = value
            End Set
        End Property


        Public Property SortField As String
            Get
                Return CType(Me.Rows(0)(2), String)
            End Get
            Set(value As String)
                Me.Rows(0)(2) = value
            End Set
        End Property


        Public Property SortDirection As String
            Get
                Return CType(Me.Rows(0)(3), String)
            End Get
            Set(value As String)
                Me.Rows(0)(3) = value
            End Set
        End Property


        Protected Overrides Sub Initialize()

            Me.Columns.Add(New DataColumn("PageNumber", GetType(Integer?)))
            Me.Columns.Add(New DataColumn("PageSize", GetType(Integer?)))
            Me.Columns.Add(New DataColumn("SortField", GetType(String)))
            Me.Columns.Add(New DataColumn("SortDirection", GetType(String)))

            Me.Rows.Add({Nothing, Nothing, Nothing, Nothing})

        End Sub

    End Class

End Namespace

在构造函数中实例化对象并设置值,然后仅从对象获取参数,并将其附加到存储过程命令对象的参数集合中。

cmd.Parameters.Add(New DateRange(startDate, endDate).CreateParameter("DateRangeParams"))
cmd.Parameters.Add(New Paging(pageNumber, pageSize).CreateParameter("PagingParams"))

编辑 由于这个答案涉及到强类型,我想添加一个在方法签名中使用强类型的示例:

'method signature with UDTs
Public Function GetMyReport(customParam1 as Integer, timeFrame as DateRange, pages as Paging) as IDataReader

'method signature without UDTs
Public Function GetMyReport(customParam1 as Integer, startDate as DateTime, endDate as DateTime, pageNumber as Integer, pageSize as Integer)

感谢您抽出时间进行评论并提供代码示例。虽然很多答案都围绕UDT展开,但您的回答因为SQL和C#示例而显得与众不同。 - mellodev
我同意,如果我们需要这些参数,接口应该保持原样。 - zinking

3
我们也遇到了这个问题。通过在数据库的Programmability/Type部分创建一个用户定义表类型,解决了此问题。 SQL Server 2008 R2用户定义表类型 当调用不同的存储过程和函数时,此表在所有应用程序中使用。我们在应用程序客户端(vb.net 2010)上以编程方式填充此表,然后将其作为参数传递。在存储过程中,我们只需读取表格并执行所需的任何操作,例如过滤、处理等。希望这可以帮助到您。

谢谢回答。我考虑过类似的东西,创建一个作为参数传递的UDT。你是如何在每个存储过程内实现“只需读取表并执行我们需要的操作”模式的?我假设UDT本质上会变成一个键/值存储,并且我仍然会在每个存储过程内得到很多样板代码。 - mellodev
抱歉,我的想法还没说完就提交了评论。你的UDT表结构是什么样子的?你是否构建了任何UDF来提取值并减少样板代码?回顾过去并根据你的经验,你会在未来使用这种模式吗? - mellodev
@mello702 不用客气。首先,我尽量避免在TSQL中使用任何额外的过程,我的意思是不使用UDF、不使用动态参数等。有些情况下可能只能这样做,但我认为这不是这个问题的解决办法。当我说“只是读取表格”时,就是指简单地从传递的表格中进行SELECT操作。毋庸置疑,这个SELECT语句可以非常复杂,甚至可以使用CTE、合并操作、调用其他存储过程或函数,或者其他我需要完成目标的方法。 - Yaroslav
谢谢。我也避免使用UDT、UDF、触发器等,除非是非常特定的情况。 - mellodev

1

我会使用XML作为参数,并添加一些UDF来帮助解包您感兴趣的XML部分。标量值UDF用于单个值参数,表值UDF用于列表。

将XML嵌入查询中往往会使查询优化器混淆,如果UDF最终出现在where子句或join中,则使用UDF可能会导致性能下降,因此我不会在查询本身中使用XML或UDF。我会先从XML获取值到本地变量、表变量或临时表中,然后在查询中使用这些值。


谢谢 - 你对于UDFs在连接或where子句中使用时的担忧是正确的。对于我的实现,我会在使用之前将XML/filter参数值解包成单独的作用域变量。我的一个担忧是查询计划缓存或优化器失败,但我相信通过在where子句中使用之前将其解包为作用域变量(类似于DateTime参数嗅探的问题/解决方案)可以避免这种情况。 - mellodev

1
我曾经遇到过类似的情况,发现UDT(用户定义表类型)非常完美地解决了问题。我们最初面临的是一个非常相似的问题:“获取此帐户的数据”,然后变成了“获取这些帐户的数据”,然后是“使用这些条件”等等。我们使用UDT而不是传递XML字符串 - 一旦进入SP,您可以直接从UDT连接,并且UDT受ADO.NET支持,因此非常简单。我们从UDT中向SP传递了数十万行数据(大规模更新),除了一个例外之外,性能没有成为问题:当您发送如此多的行时,永远不要尝试跟踪查询 - SQL服务器内部的线程调度程序将会崩溃。
在使用用户定义表类型时要注意一件事情:由于某种原因,Microsoft认为防止更改它们是个好主意,您只能删除/添加它们。然后有人认为如果有其他东西依赖于它们,防止您删除它们会更好,因此如果您手动更改它们,则需要非常痛苦的过程来删除/重新构建它们。
我们没有将所有参数封装到单个UDT中,只是因为我们的需求从过程到过程更具体。所以当我们有一些列表时,我们使用了一个UDT作为该参数,但我可以很容易地看到一个One UDT To Rule Them All会很有用,带有一些方便的函数来提取众所周知的值,比如日期。我厌恶重复编写相同的代码,这肯定会缩小你的代码库,代价是增加了一些复杂性。一个副作用是强制所有开发人员遵守标准的做事方式,这在时间紧迫时并不总是被执行。你还可以在数据层内打开一些很好的代码重用机会。

我喜欢你的回答 - 你识别了相同的模式并提出了类似的解决方案。你是如何处理更新UDT的?如果我理解正确,它们无法更新,而必须被删除/重新创建,这会受到引用检查的影响。当你说“手动”时,是指通过脚本而不是使用UI吗? - mellodev
由于UDT只能被删除/创建,而不能被修改,所有依赖对象(例如使用UDT的存储过程/函数)在UDT被删除之前也必须被删除。没有简单的方法可以删除使用特定UDT的所有对象,需要手动识别。您可以查询信息模式以查找特定类型的参数,然后编写循环以删除包含这些参数的例程,但我不认为这是一个“简单”的解决方案。 - ulty4life
这正是我们所做的 - 设置它花了几个小时,不算太痛苦但也不是微不足道的。您可以在sys模式中查找依赖项,然后编写一个循环,将sys.all_sql_modules中的依赖对象定义写入表中。然后您循环执行,删除依赖项,为UDT删除/添加,然后重新构建依赖对象的定义。我没有做过这个,也不断言它是真的,但我听说一些工具如dbProj会自动完成此操作。 - Triple Gilaman
选择不同的r.ROUTINE_TYPE,r.ROUTINE_SCHEMA,r.ROUTINE_NAME 从INFORMATION_SCHEMA.PARAMETERS p 内部联接INFORMATION_SCHEMA.ROUTINES r r.ROUTINE_NAME = p.SPECIFIC_NAME 和r.ROUTINE_SCHEMA = p.SPECIFIC_SCHEMA 其中p.DATA_TYPE ='table type' 和p.USER_DEFINED_TYPE_NAME = 'MyUdtTypeName' - ulty4life

1
在我看来,对于这个问题没有真正美观的解决方案。最大的问题是通常一些参数可以为空,但有些不行(无论参数来自表值参数还是 XML 参数都无所谓)。然后就会得到类似于以下 SQL 的结果:
Declare @Col1Value int = null
Declare @Col2Value int = null
Select * 
From dbo.MyTable
where (@Col1Value is Null Or Col1 = @Col1Value)
    And (@Col2Value is Null Or Col2 = @Col2Value)

当然,它不是高效的+查询计划远非最佳..

为了解决这个问题,动态SQL可以帮助很多。在这种情况下,应该非常谨慎地考虑用户权限(可以使用Execute As someProxyUser,Certificates)。

然后可以使用一个输入XML参数创建过程,其中传递所有所需的参数,然后生成SQL.. 但仍然-这不是一种很好的方法来做事情,因为当SQL变得更加复杂时,涉及到大量编码..例如,如果您从多个表中选择数据,并且其中有相同的列..

总之,我认为没有漂亮和优雅的解决方案来解决这个问题.. 使用实体框架和传递参数的经典方式 :).


感谢您抽出时间回复。清楚地说,我并不希望将一个代表整个Where子句的单个参数传递给所有过程。举个简单的例子,我只想封装需要的常见分页和日期过滤器值(PageSize、PageNum、startdate、enddate)。几乎我们所有用于报告的查询都需要这些相同的参数,并且都包含您提到的样板代码类型。类似于 declare @pagenum int;set @pagenum=isnull(@pagenumparam,1); ... Where 1=1 And cte.PageNum = @pagenum;。我同意没有很好的解决方案 :) - mellodev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接