SQL Server 事务、异常和游标

381 阅读10分钟

一、事务

在数据库中有时候需要把多个步骤的指令当做一个整体来运行,这个整体要么全部成功,要么全部失败,这就需要用到事务。

  1. 事务的特点

    事务有若干条T—SQL指令组成,并且所有的指令作为一个整体提交给数据库系统,执行时,这组指令要么全部执行完成要么全部取消。因此,事务是一个不可分割的逻辑单元。

    事务有四个属性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),也称作事物的ACID属性。

    原子性(Atomicity):事务内所有工作要么完成要么全部不完成,不存在只有一部分完成的情况。

    一致性(Consistency):事务内的所有操作都不能违反数据库的约束或规则,事务完成时有内部数据结构都必须是正确的。

    隔离性(Isolation):事务直接是相互隔离的,如果有两个事务对同一个数据库进行操作,比如读取表数据。任何一个事物看到的所有内容要么是其他事务完成之前的状态要么是其他事务完成之后的状态。一个事务不可能遇到另一个事务的中间状态。

    持久性(Durability):事务完成之后,它对数据库的影响是持久的,即使是系统错误重启系统后该事务的结果依然存在。

  2. 事务的模式

    a. 显示事务

    显示事务就是用户使用T-SQL明确的定义事务的开始(begin transaction)和提交(commit transaction)或回滚事务(rollback transaction)

    b. 自动提交事务

    自动提交事务是一种能够自动执行并能自动回滚事务,这种方式是T-SQL的默认事务方式。例如在删除一个表记录的时候,如果这条记录有主外键关系的时候,删除就会受主外键约束的影响,那么这个删除就会取消。

    c. 隐式事务

    隐式事务是指当事务提交或回滚后,SQL Server自动开始事务。因此,隐式事务不需要使用begin transaction显示开始,只需直接提交事务或回滚事务的T-SQL语句即可。使用时,需要设置set implicit_transaction on语句,将隐式事务模式打开,下一个语句会启动一个新的事务,再下一个语句又将启动一个新事务。

  3. 事务处理

    常用T-SQL事务语句:

    a. begin transaction语句

    开始事务,而@@trancount全局变量用来记录事务的数目值加1,可以用@@error全局变量记录执行过程中的错误信息,如果没有错误可以直接提交事务,有错误可以回滚。

    b. commit transaction语句

    回滚事务,表示一个隐式或显式的事务的结束,对数据库所做的修改正式生效。并将@@trancount的值减1

    c. rollback transaction语句

    回滚事务,执行rollback tran语句后,数据会回滚到begin tran的时候的状态。

  1. 事务的示例
 # 开始事务
 begin transaction tran_bank;
 declare @tran_error int;
     set @tran_error = 0;
     begin try
         update bank set totalMoney = totalMoney - 10000 where userName = 'jack';        
         set @tran_error = @tran_error + @@error;
         update bank set totalMoney = totalMoney + 10000 where userName = 'jason';
         set @tran_error = @tran_error + @@error;
     end try
     begin catch        
         print '出现异常,错误编号:' + convert(varchar, error_number()) + ', 错误消息:' + error_message(); 
         set @tran_error = @tran_error + 1;
     end catch
 if (@tran_error > 0)
     begin
         --执行出错,回滚事务
         rollback tran;
         print '转账失败,取消交易';
     end
 else
     begin
         --没有异常,提交事务
         commit tran;
         print '转账成功';
     end
 go

二、异常

在程序中,有时候完成一些T-SQL会出现错误、异常信息。如果我们想自己处理这些异常信息的话,需要手动捕捉这些信息。那么我们可以利用try catch完成。

try catch构造包括两部分:一个try块和一个catch块。如果在try块中包含的T-SQL语句中检测到错误条件,控制将被传递到catch块`(可在此块中处理该错误)。

catch块处理该异常错误后,控制将被传递到end catch语句后面的第一个T-SQL语句。如果end catch语句是存储过程或触发器中的最后一条语句,控制将返回到调用该存储过程或触发器的代码。将不执行try块中生成错误的语句后面的T—SQL语句。

如果try块中没有错误,控制将传递到关联的end catch语句后紧跟的语句。如果end catch语句是存储过程或触发器中的最后一条语句,控制将传递到调用该存储过程或触发器的语句。

try 块以 begin try 语句开头,以 end try 语句结尾。在 begin tryend try 语句之间可以指定一个或多个T-SQL 语句。catch 块必须紧跟 try 块。catch 块以 begin catch 语句开头,以 end catch 语句结尾。在 T-SQL 中,每个 try 块仅与一个 catch 块相关联。

 # 错误函数
 # try ... catch 使用错误函数来捕获错误信息。
 ERROR_NUMBER() -- 返回错误号。
 ERROR_MESSAGE() -- 返回错误消息的完整文本。此文本包括为任何可替换参数(如长度、对象名称或时间)提供的值。
 ERROR_SEVERITY() -- 返回错误严重性。
 ERROR_STATE() -- 返回错误状态号。
 ERROR_LINE() -- 返回导致错误的例程中的行号。
 ERROR_PROCEDURE() -- 返回出现错误的存储过程或触发器的名称。
 # 示例
 -- 错误消息存储过程
 if (object_id('proc_error_info') is not null)
     drop procedure proc_error_info
 go
 create proc porc_error_info
 as
     select 
         error_number() '错误编号',
         error_message() '错误消息',
         error_severity() '严重性',
         error_state() '状态好',
         error_line() '错误行号',
         error_procedure() '错误对象(存储过程或触发器)名称';       
 go
 # 示例:用异常处理错误信息
 -- 简单try catch示例
 begin try
     select 1 / 0;
 end try
 begin catch
     exec proc_error_info; -- 调用错误消息存储过程
 end catch
 go
 # 示例:异常能处理的错误信息
 -- 简单try catch示例, 无法处理错误
 begin try
     select * * from student;
 end try
 begin catch
     exec proc_error_info;
 end catch
 go
 ​
 -- 简单try catch示例, 无法处理错误
 begin try
     select * from st;
 end try
 begin catch
     exec proc_error_info
 end catch
 go
 ​
 -- 异常处理,能处理存储过程(触发器)中(不存在表对象)的错误信息
 if (object_id('proc_select') is not null)
     drop procedure proc_select
 go
 create proc proc_select
 as
     select * from st;
 go
 begin try
     exec proc_select;
 end try
 begin catch
     exec proc_error_info
 end catch
 go
 # 异常不能处理编译期的错误,如语法错误。以及重编译造成部分名称对象得不到正确解析的时候所出现的错误。
 # 示例:无法提交的事务
 -- 创建临时用表
 if (object_id('temp_tab', 'u') is not null)
     drop table temp_tab
 go
 create table temp_tab(
     id int primary key identity(100000, 1),
     name varchar(200)
 )
 go
 ​
 begin try
     begin tran;
     alter table temp_tab drop column createTime;
     commit tran;
 end try
 begin catch
     exec proc_error_info;--显示异常信息
     if (xact_state() = -1)
     begin
         print '会话具有活动事务,但出现了致使事务被归类为无法提交的事务的错误。'
             + '会话无法提交事务或回滚到保存点;它只能请求完全回滚事务。'
             + '会话在回滚事务之前无法执行任何写操作。会话在回滚事务之前只能执行读操作。'
             + '事务回滚之后,会话便可执行读写操作并可开始新的事务。';
     end
     else if (xact_state() = 0)
     begin
         print '会话没有活动事务。';
     end
     else if (xact_state() = 1)
     begin
         print '会话具有活动事务。会话可以执行任何操作,包括写入数据和提交事务。';
     end
 end catch
 go
  # 示例:处理异常日志信息
  -- 异常、错误信息表
 if (object_id('errorLog', 'U') is not null)
     drop table errorLog
 go
 create table errorLog(
     errorLogID int primary key identity(100, 1),   -- ErrorLog 行的主键。
     errorTime datetime default getDate(),          -- 发生错误的日期和时间。
     userName sysname default current_user,         -- 执行发生错误的批处理的用户。
     errorNumber int,                               -- 发生的错误的错误号。
     errorSeverity int,                             -- 发生的错误的严重性。
     errorState int,                                -- 发生的错误的状态号。
     errorProcedure nvarchar(126),                  -- 发生错误的存储过程或触发器的名称。
     errorLine int,                                 -- 发生错误的行号。
     errorMessage nvarchar(4000)
 )
 go
 ​
 -- 存储过程:添加异常日志信息
 if (object_id('proc_add_exception_log', 'p') is not null)
     drop proc proc_add_exception_log
 go
 create proc proc_add_exception_log(@logId int = 0 output)
 as
 begin
     set nocount on;
     set @logId = 0;
     begin try
         if (error_number() is null)
             return;
         
         if (xact_state() = -1)
         begin
             print '会话具有活动事务,但出现了致使事务被归类为无法提交的事务的错误。'
                 + '会话无法提交事务或回滚到保存点;它只能请求完全回滚事务。'
                 + '会话在回滚事务之前无法执行任何写操作。会话在回滚事务之前只能执行读操作。'
                 + '事务回滚之后,会话便可执行读写操作并可开始新的事务。';
         end
         else if (xact_state() = 0)
         begin
             print '会话没有活动事务。';
         end
         else if (xact_state() = 1)
         begin
             print '会话具有活动事务。会话可以执行任何操作,包括写入数据和提交事务。';
         end
         
         --添加日志信息
         insert into errorLog values(getDate(), 
             current_user, error_number(), 
             error_severity(), error_state(), 
             error_procedure(), 
             error_line(), error_message());
         --设置自增值
         select @logId = @@identity;
     end try
     begin catch
         print '添加异常日志信息出现错误';
         exec proc_error_info;--显示错误信息
         return -1;
     end catch
 end
 go
 ​
 -- 处理异常信息示例
 declare @id int;
 begin try
     begin tran;
     -- 删除带有外键的记录信息
     delete classes where id = 1;
     commit tran;
 end try
 begin catch
     exec proc_error_info; -- 显示错误信息
     if (xact_state() <> 0)
     begin
         rollback tran;
     end
     exec proc_add_exception_log @id output
 end catch
 select * from errorLog where errorLogID = @id;
 go

三、游标

游标可以对一个select的结果集进行处理,或是不需要全部处理,就会返回一个对记录集进行处理之后的结果。

  1. 游标实际上是一种能从多条数据记录的结果集中每次提取一条记录的机制。游标可以完成:

    • 允许定位到结果集中的特定行
    • 从结果集的当前位置检索一行或多行数据
    • 支持对结果集中当前位置的进行修改

    由于游标是将记录集进行一条条的操作,所以这样给服务器增加负担,一般在操作复杂的结果集的情况下,才使用游标。SQL Server 2005有三种游标:T-SQL游标API游标客户端游标

  2. 游标的基本操作

    游标的基本操作有定义游标、打开游标、循环读取游标、关闭游标、删除游标。

 # 定义游标
 declare cursor_name                        -- 游标名称
 cursor [local | global]                    -- 全局、局部
 [forward only | scroll]                    -- 游标滚动方式
 [read_only | scroll_locks | optimistic]     -- 读取方式
 for select_statements                       -- 查询语句
 [for update | of column_name ...]           -- 修改字段
 ​
 # 参数:
 -- forward only | scroll:前一个参数,游标只能向后移动;后一个参数,游标可以随意移动
 -- read_only:只读游标
 -- scroll_locks:游标锁定,游标在读取时,数据库会将该记录锁定,以便游标完成对记录的操作
 -- optimistic:该参数不会锁定游标;此时,如果记录被读入游标后,对游标进行更新或删除不会超过
  # 打开游标
  open cursor_name;
 -- 游标打开后,可以使用全局变量@@cursor_rows显示读取记录条数
 # 检索游标
 fetch cursor_name;
 -- 检索方式如下:
 fetch first; -- 读取第一行
 fetch next;  -- 读取下一行
 fetch prior; -- 读取上一行
 fetch last; -- 读取最后一行
 fetch absolute n; 
 -- 读取某一行
 -- 如果n为正整数,则读取第n条记录
 -- 如果n为负数,则倒数提取第n条记录
 -- 如果n为,则不读取任何记录
 fetch pelative n
 -- 如果n为正整数,则读取上次读取记录之后第n条记录
 -- 如果n为负数,则读取上次读取记录之前第n条记录
 -- 如果n为,则读取上次读取的记录
 # 关闭游标
 close cursor_name;
 # 删除游标
 deallocate cursor_name;
 # 示例
 -- 创建一个游标
 declare cursor_stu cursor scroll for
     select id, name, age from student;
 -- 打开游标
 open cursor_stu;
 -- 存储读取的值
 declare @id int, @name nvarchar(20), @age varchar(20);
 -- 读取第一条记录
 fetch first from cursor_stu into @id, @name, @age;
 -- 循环读取游标记录
 print '读取的数据如下:'
 -- 全局变量
 while (@@fetch_status = 0)
 begin
     print '编号:' + convert(char(5), @id) + ', 名称' + @name + ', 类型:' + @age;
     -- 继续读取下一条记录
     fetch next from cursor_stu into @id, @name, @age;
 end
 -- 关闭游标
 close area_cursor;
 -- 删除游标
 deallocate area_cursor;