9

In college last year I came across something I found very interesting with regard to stored procedures. It was mostly with regard to reducing code repetition for transaction management, error handling, and security. But I have researched it since and cannot find it anywhere. Perhaps I don't know what it is called so I will explain.

Let's say I have a simple table (forget its relationships, just a single table). I have at least 5 possible operations I can do on it namely CRUD, with the R being read the details of one row, or read a list of rows for a given criteria. Again, let's not get into too much detail with complicated stored procedures and let's pretend, for this example, we want nothing but the following 5 operations to be performed:

  • Create (simple insert)
  • Read (one for reading a single row)
  • Read multiple (reading a list of rows, simple select * from sometable where some condition, nothing complicated),
  • Update (simple update)
  • Delete (again, simple delete).

For the purposes of this example, a simple table with 2 or 3 columns, and the most simple procedures you can come up with to do those operations.

The problem:

My lecturer, when talking to us about transactions, advised us to take these simple procedures, and keep them separate as is normal. But then to write a master procedure which executes each of these. This means we have a master procedure into which we pass a character lets say 'C' for create and so on, and a series of conditions which decide which job to run. Of course, we should pass in the details needed from the master to the child proc which means we need to get them from the user as parameters. This master procedure is already beginning to sound complicated.

His reasoning, do the transaction handling, validation, error handling, and security handling in the master procedure. Then call that master procedure with the required parameters, which does all the checking, and passes the parameters to the child procedure. Below is an example.

enter image description here

Simple table, don't go worrying too much about relationships, keys, indexes, constraints, triggers and so on.

For the below two pieces of code, the only real relevant bit is the parts between the try...catch. The rest is boiler plate for the master.

Below is the code for a create procedure:

ALTER proc [Actions].[AreaCreate]
--External Variables - Input
@AreaName varchar(50),  @AreaAvailablity bit,   @Description varchar(max),

--External Variables - Output
@NoOfRecords int output

as
--Internal Variables
declare @ErrorMessage varchar(max)

Begin try
    insert  [Tennis3rdYrMVC].[dbo].Areas
            ([AreaName],    [AreaAvailablity],  [Description])
    values  (@AreaName,     @AreaAvailablity,   @Description)

    --Show # of records affected so you can detect nulls or empty lists
    --and handle them as you choose in the code
    set     @NoOfRecords = @@ROWCOUNT
End try
Begin Catch
    set @ErrorMessage = ERROR_MESSAGE()
    raiserror(@ErrorMessage,16,1)
    return ERROR_NUMBER()
End Catch

--All Ok
return 0

Below is another piece of code for a delete procedure

ALTER proc [Actions].[AreaDelete]
--External Variables - Input
@Id int,

--External Variables - Output
@NoOfRecords int output

as
--Internal Variables
declare @ErrorMessage varchar(max)

begin try
    delete from [Tennis3rdYrMVC].[dbo].Areas
    where Id = @Id

    --Show # of records affected so you can detect nulls or empty lists
    --and handle them as you choose in the code
    set @NoOfRecords=@@ROWCOUNT
end try
begin catch
    set @ErrorMessage = ERROR_MESSAGE()
    raiserror(@ErrorMessage,16,1)
    return ERROR_NUMBER()
end catch

--All Ok
return 0

Finally, the suggested, and complicated master proc.

ALTER proc [Admin].[AreaMaster]
--External Variables - Input
@JobType int, -- 1=Create, 2=Read 1, 3=Read Many, 4=Update, 5=Delete
--Master sprock uses null defaults because not every job requires every field to be present.
--i.e. We dont need to know the area name of an area we want to delete.
@Id int                 = null,     @AreaName varchar(50)       = null,
@AreaAvailablity bit    = null,     @Description varchar(max)   = null,

--External Variables - Output
@NoOfRecords int = null output --Used to count the number of records affected where needed.
as
BEGIN
    --Internal Variables
    declare @ErrorMessage varchar(max)
    declare @return_value int

    -- SET NOCOUNT ON added to reduce network traffic and speed things up a little.
    SET NOCOUNT ON;

    /*
    --VALIDATION

    --Logic for ensuring all required values are entered should be done in processing below in
    --the master sprock, NOT in the children (i.e. why check for valid id in 5 sprocks when you
    --can do it here in one).


    We will do all the processing needed to ensure valid and required values are entered where
    needed.

    --SECURITY
    This is also where we put the security code required to stop SQL Injection and other
    attacks, NOT in the child sprocks. The child sprocks would not be allowed to execute by
    pages directly.

    */

    --Once all validation is done, call  relevant child sprocks

    --Call AreaCreate Sprock
    if(@JobType='1')
    begin
        exec    @return_value = [Actions].[AreaCreate]
                @AreaName           = @AreaName,
                @AreaAvailablity    = @AreaAvailablity,
                @Description        = @Description,
                @NoOfRecords        = @NoOfRecords OUTPUT

        --select    @return_value 'Return Value'

        if @return_value<>0
        begin
            raiserror('Error: Problem creating area.',16,0)
            --rollback transaction
            return 99
        end
    end


    --Call AreaShowDetail Sprock
    if(@JobType='2')
    begin
        exec    @return_value   = [Actions].[AreaShowDetail]
                @Id             = @Id,
                @NoOfRecords    = @NoOfRecords output
        ----Testing
        --select    'Return Value' = @return_value

        if @return_value<>0
        begin
            raiserror('Error: Problem reading area details.',16,0)
            --rollback transaction
            return 99
        end
    end


    --Call AreaShowList Sprock
    if(@JobType='3')
    begin
        exec    @return_value   = [Actions].[AreasShowList]
                @NoOfRecords    = @NoOfRecords output
        ----Testing
        --select    'Return Value' = @return_value

        if @return_value<>0
        begin
            raiserror('Error: Problem reading areas list.',16,0)
            --rollback transaction
            return 99
        end
    end


    --Call AreaUpdate Sprock
    if(@JobType='4')
    begin
        EXEC    @return_value = [Actions].[AreaUpdate]
                @Id                 = @Id,
                @AreaName           = @AreaName,
                @AreaAvailablity    = @AreaAvailablity,
                @Description        = @Description,
                @NoOfRecords        = @NoOfRecords OUTPUT

        --select    'Return Value'      = @return_value

        if @return_value<>0
        begin
            raiserror('Error: Problem updating area.',16,0)
            --rollback transaction
            return 99
        end
    end


    --Call AreaDelete Sprock
    if(@JobType='5')
    begin
        exec    @return_value   = [Actions].[AreaDelete]
                @Id             = @Id,
                @NoOfRecords    = @NoOfRecords output

        --select    'Return Value'  = @return_value
        if @return_value<>0
        begin
            raiserror('Error: Problem deleting area(s).',16,0)
            --rollback transaction
            return 99
        end
    end

    --All Ok
    return 0
END

Is that complicated or what? Now imagine another level which if you have several tables to operate on, will decide which sub master to call, and this MasterMaster for want of a better name must have external parameters for every field in every table. However, the validation, security, and transaction code would be moved up to that level instead.

More importantly, is that the way a professional would do it. And finally, I am using MVC with entity framework. Suppose I have a database like this (I have), and access is only allowed through this master stored procedure (it is). How do I call such a stored procedure from within EF and MVC. Even better, how would I bypass EF all together and get the data into my controller in a way my view will understand. I know how to do this with ASP.Net code behind. But not in MVC.

Any suggestions. Please, feel free to tell me that this is completely crazy as it seems like a hell of a lot of work (what if you have 50 tables (I do). Imagine, 5 simple operations, across 50 tables = 250 child stored procedures + 50 Sub-Sub-Masters, + 5 Sub-Masters + 1 Master. I have my work cut out. Is there a tool that can do this or at least generate a template for me?

So to summarise, My questions are:

  1. Is this overly complicated or is this the professional way to do this?
  2. What is the term used to describe this Master / child procedural relationship.
  3. Is there a tool that can automate this process.
  4. Is there a tool that can help me generate some kind of boilerplate template.
  5. How can I use this procedure with EF and MVC
  6. How can I bypass EF altogether and just find a way to call the procedure directly.
  7. Is there anything like the equivalent of classes, or even methods in T-SQL which can structure this much like a standard piece of code. Meaning that each child procedure is a method, each table has its properties, and the constructor will be the the initial (or altered) representation of the object.
  8. If 7 does exist, which would be more standard, the way I described (i.e.) splitting everything up, or the idea in 7.

With regard to this point 4 I am talking about something simple, like the one already in SQL Server where you right click a procedure and choose execute and it generates some simple code for testing your procedure, or like the one for making functions.

Thanks for reading this and for any help you can offer.

4

2 回答 2

6

从理论上讲,您的讲师提出了一个很好的概念。将某些类型视为顶级域模型的想法很有意义。了解如何拯救其直系子女的对象也非常有用。

除了理论,我不同意实现,主要是因为它(不必要的)冗长和不灵活。

存储过程

存储过程很棒,但是使用 ORM(如 EF)或任何自动化框架的好处之一是您不必一遍又一遍地编写样板代码来做同样的事情。多年来,我已经大大减少了我的应用程序中仅 CRUD 的过程的数量,而是更喜欢根据需要自动生成语句(同样,这是一个好的 ORM 的功能)。

阅读
大多数应用程序需要以不同的方式查看相同的数据,并且拥有无限数量的选项来查询该数据通常是有利的。也许你想要整张唱片;也许你想要一个字段。也许您想要一级对象层次结构,或者您希望将对象层次结构扁平化为视图模型。

多读
当我编写存储过程时,我倾向于将精力放在高度优化的存储过程上,以有效地浏览/搜索数据。

在大多数业务应用程序中,read-many 最终成为数据的摘要视图。作为搜索结果返回 1000 条窄/扁平记录是微不足道的;但是检索 1000 个复杂对象的完整层次结构效率极低,通常不需要。

公平地说,您的主 proc 可以返回适当的摘要视图,但我指的是我之前的观点,即能够以多种方式查看数据。声明单一的多读行为是不必要的限制。

删除
删除有时是存储过程的理想选择。将 100 条记录加载到业务层只是为了获取它们的 ID 并一一发出 DELETE 是没有意义的。

事务
在我看来,最灵活的事务支持通常来自业务层。请参阅我对“应在 .NET 或 SQL Server 中处理事务?”的回答:https ://stackoverflow.com/a/12470061/453277

顶级模型

“Person”是顶级模型的良好候选者,而“Address”可能是该模型的子模型。

一个主程序促进所有访问的想法是完全不切实际的。虽然您可能只向用户显示人员管理屏幕,但在后台您可能希望访问地址对象而不需要或不知道与它相关的人员。

一个简单的示例是带有地址列表的个人资料页面;您单击“编辑”按钮以启动模式窗口以编辑地址的详细信息。你真的想要通过人员管理逻辑来获取地址数据吗?您可能会执行与父母相关的安全检查;您可能会与父母建立新的关系;你可能什么都不做。灵活性和可重用性是这里的关键。

此外,当您决定在 Person->Address 关系之外添加 Company->Address 关系时,您不希望重新创建任何此逻辑。

亲子关系

两个相关对象之间并不总是存在可定义的父/子关系。我最喜欢的例子是 Person 和 Document。

  • 一个人可能有一个他们创作的文档列表。
  • 一个文档可能有一个作者列表。

哪个是家长?

对数据库来说,它几乎不重要。某处可能有一个PersonDocument包含两个整数列的表;哪个是父母并不重要。同样,在业务层中,灵活性是关键。您应该能够以“孩子”的形式访问Document人员列表,并以“孩子”Person的形式访问文档列表。

我喜欢将所有关系视为潜在的双向关系。将它们视为双向的从来没有什么坏处,但强制父/子层次结构可能会受到限制。

安全

SQL 注入
我在您的主进程的评论中注意到了这一点:

这也是我们放置阻止 SQL 注入和其他攻击所需的安全代码的地方,而不是在子链中。不允许子链轮直接由页面执行。

对存储过程的调用应始终正确参数化,这提供了所需的防止注入的保护。这里提到的 SQL 注入要么无关紧要,要么令人担忧,因为它表明调用没有得到适当的准备。

Securables
对 SQL Server 中对象级安全性的讨论远远超出了本文的范围,但我会提到,您可以在不使用主过程的情况下实现极其精细的安全性。

建议

  • 如果您使用的是 ORM(如 EF),请让它完成工作并为您编写此管道代码。不要试图将不同的范式强加给它。如果您必须使用讲师的方法(例如对于作业),从等式中删除 EF 可能更容易。

  • 灵活性是关键(我已经第三次说了)。无论您开发了多么出色的范例,您都需要在某些时候扩展它或偏离它。能够做到这一点至关重要。

  • 即使两种类型看似无关,您也应该能够在相同的事务和逻辑上下文中操作它们。

  • 正如@Habibillah 指出的那样,不要忘记测试/可测试性。

  • 当您需要交付项目时,不要重新发明轮子(即使用完成工作的现有工具)。然而,这是一个很好的问题,创建自己的对象持久性/检索方法是一个很好的学术练习。

于 2012-10-20T09:56:03.683 回答
0

它只是一种称为以数据库为中心的软件开发范例。简而言之,您只需为 c# 之类的编译代码编写一点点即可调用数据库过程或函数,所有逻辑都将存储在过程和函数上。

出于某种原因,我发现很多公司都非常严格地修改他们的代码(需要更多的经理批准)而不是由 DBA 修改数据库程序,因此维护应用程序变得很简单。

然而,软件开发现在采用以应用程序为中心的方法,所有规则包括存储在代码中的数据库方案(已使用 ORM 完成)、单元测试、继续集成等,但没有这些工具(例如单元测试)维护代码是比存储过程更复杂。

于 2012-10-19T23:19:35.840 回答