PL/SQL 训练04--事务

--pl/sql通过SQL和ORACLE数据库紧密的整合在一起
--在pl/sql中可以执行任何操作语句(DML语句),包括INSERT,UPDATE,DELETE,MERGE,也包括查询语句
--可否执行DDL语句呢?
--不可以直接执行,但可以通过动态SQL的方式执行,关于动态SQL,后面课程会专门拿一节课来讲

--事务的ACID原则:原子性,一致性,隔离性,持久性

--原子性:事务所涉及的改变是原子的:这些改变或者全部发生或者全部不发生

--一致性:一个事务必须是一个正确的状态转换。事务中发生的行为作为一个整体不能违反状态的任何完整性约束

--隔离:很多事务可以同时发生,不过从任何一个事务的角度看,其他的事务看起来都在它之前或之后发生的

--持久性:一旦一个事务成功结束,状态的改变是永久的,可能经受住以后发生的任何故障

--COMMIT或ROLLBACK,一个事务可以通过执行COMMIT保存,或者ROLLBACK回滚。
--资源上的锁释放
--事务和会话的关系:默认每个会话中只有一个事务。所有修改都属于当前事务的一部分。
--自制事务特性:可以在会话的住事务中嵌套其它事务

--建立日志表
create table ma_user_log
( created_by     varchar2(100) default 'system' not null,
  created_date   date default sysdate not null,
  updated_by     varchar2(100) default 'system' not null,
  updated_date   date  default sysdate not null,
  id_ma_user_log varchar2(32) default sys_guid() not null,
  user_name      varchar2(100) not null,
  trace_mark     varchar2(1000) );
  

create or replace procedure test_pragma(i_user         in varchar2,
                                        i_trace_remark in varchar2) is

  pragma autonomous_transaction; --定义自制事务
begin
  insert into ma_user_log
    (user_name, trace_mark)
    select i_user, i_trace_remark from dual;
  --insert into ma_user_log(user_name, trace_mark)values(user_name,i_trace_remark);
  commit;

end test_pragma;
/
declare

  cursor cur_users is
    select * from ma_users r where r.user_status = '1'; --当前用户对表有查询权限
begin

  for v in cur_users loop
    update ma_users r --注意对此表进行操作需要有UPDATE的权限
       set r.user_point = 100
     where r.user_name = v.user_name;
    test_pragma(v.user_name, '数据修改,用户积分初始值100');
  end loop;

end;
/


select * from ma_user_log;

--dml语句的快速入门

--insert语句:向表中插入一条或者多条记录了
insert into ma_user_log(user_name, trace_mark)values('test','test test'); --插入一条数据
insert into  ma_user_log(user_name, trace_mark)select 'test1', 'test1test1' from dual;--插入一条数据
insert into  ma_user_log(user_name, trace_mark)
select t.user_name,t.user_remark from ma_users t where t.user_status ='1';--插入多条数据
insert into ma_user_log--必须写出所有的列且一一对应
  select 'system',
         sysdate,
         'system',
         sysdate,
         sys_guid(),
         t.user_name,
         t.user_remark
    from ma_users t
   where t.user_status = '1';--插入多条数据


--UPDATE语句:更新一行或多行的一或多列
update ma_users t 
set t.user_point = 100,t.user_status ='0'
where t.user_name ='乱世佳人';

--DELETE语句:删除一个表的一行、多行、或者所有记录行

delete from ma_user_log ;
delete from ma_user_log where 1=1;
create or replace procedure del_user_log(i_date in date,o_log_num out number)
is 
begin 
   delete from ma_user_log t where t.created_date < i_date ;
   o_log_num := sql%rowcount ;
end ;
/

declare 
   v_num  number ;
begin 
   
  del_user_log(sysdate,v_num);
  dbms_output.put_line('删除'||v_num||'个记录');
end ;
/

--MERGE 语句:指定一个匹配条件,然后针对匹配和不匹配的记录分别采取不同的行为
declare
begin
  merge into ma_user_log mu
  using (select * from ma_users) t
  on (mu.user_name = t.user_name)
  when matched then
    update set mu.trace_mark = 'hello tt'
  when not matched then
    insert (mu.user_name, mu.trace_mark) values (t.user_name, 'hello'||t.user_name);

end;
/
select * from ma_user_log;

---dml操作的游标属性
--通过一些特殊的隐式游标属性访问最后一次运行的隐式游标的信息
--sql%found:如果有一行或多行记录被成功修改(包括创建、修改、删除)返回TRUE
--sql%notfound:如果DML语句没有修改任何行则返回TRUE
--sql%rowcount:返回DML语句修改的记录行数
--sql%isopen:对于隐式游标(及DML语句)总是返回FALSE,因为ORACLE数据库会自动打开和关闭这些游标

create or replace procedure update_point(i_user in varchar2,
                                         o_bool out boolean,
                                         o_num  out number)

 is

begin

  update ma_users t set t.user_point = 100 where t.user_name = i_user;
  o_bool := sql%found;
  o_num  := sql%rowcount;

end;
/

declare
  v_bool boolean;
  v_num  number;
begin

  update_point('乱世佳人', v_bool, v_num);
  if v_bool then
    dbms_output.put_line('true');
  else
    dbms_output.put_line('false');
  end if;
  dbms_output.put_line(v_num);
  update_point('乱世佳', v_bool, v_num);
  if v_bool then
    dbms_output.put_line('true');
  else
    dbms_output.put_line('false');
  end if;
  dbms_output.put_line(v_num);

end;
/

--returning:从dml语句返回信息
--可以从insert,update,delete,merge语句中添加一个RETURNING字句,返回信息到一个变量中
--不需要单调去查询

declare
  v_phone ma_users.user_name%type;
  v_email ma_users.user_email%type;
begin

  for v in (select * from ma_users) loop
  
    update ma_users r
       set r.user_point = 1000
     where r.id_ma_users = v.id_ma_users
    returning r.user_phone, r.user_email into v_phone, v_email;
    --select r.user_phone, r.user_email into v_phone, v_email from ma_users r where r.id_ma_users = v.id_ma_users
    dbms_output.put_line(v.user_name || '-' || v_phone || '-' || v_email);
  end loop;

end;
/

--如果UPDATE语句修改返回的记录函数多于一行,可以使用BULK COLLECT返回到一个集合中

declare
  type point_t is table of number;

  v_point point_t;
begin

  update ma_users t
     set t.user_point = 200*(sysdate - t.user_birth_date)
   where 1 = 1
  returning t.user_point bulk collect into v_point;
  for i in v_point.first..v_point.last loop 
      dbms_output.put_line(v_point(i));
  end loop ;
end;
/

--异常处理
--如果一个PL/SQL块出现异常时,oracle数据库不会回滚这个块中DML语句所做的修改
--需要我们去决定采取什么行动

create or replace procedure del_users(i_user in varchar2,o_count out number )
is 

begin 

   select count(1) into o_count from ma_users ;
   --o_count:= 3;
   delete from ma_users mu where mu.user_name = i_user;
   raise no_data_found ;
end ;
/
declare

  v_count number :=-1;
begin

  del_users('乱世佳人1', v_count);
  dbms_output.put_line('1=='||v_count);--不会被打印,直接进入异常
exception
  when others then
    dbms_output.put_line('2=='||v_count);
    select count(1) into v_count from ma_users;
     dbms_output.put_line('3=='||v_count);
end;
/
declare
  v_count number :=-1;
begin

  del_users('乱世佳人1', v_count);
  dbms_output.put_line(v_count);

end;
/

select count(1) from ma_users;

--有几点需要注意
--如果代码块中使用的是自治事务,在发生异常时需要执行回滚或者提交
--可以通过SAVEPOINT来控制回滚的范围。可以回滚到某个特殊SAVEPOINT,
--从而把会话所做出的改变部分保存下来
--如果一个异常传播到最外层的代码块,多数PL/SQL执行环境比如SQL*PLUS,都会自动回滚
--所有变化都会被撤销

--基于记录的DML

create or replace procedure save_user(i_user ma_users%rowtype) is
    
begin
  insert into ma_users values i_user;
exception
  when dup_val_on_index then
    update ma_users t
       set row = i_user
     where t.user_name = i_user.user_name;
end;
/

declare

  v_user ma_users%rowtype;
begin
  v_user.created_by      := 'system';
  v_user.created_date    := sysdate;
  v_user.updated_by      := 'system';
  v_user.updated_date    := sysdate;
  v_user.id_ma_users     := sys_guid();
  v_user.user_name       := '幸运小子2';
  v_user.user_password   := 'text123';
  v_user.user_sex        := '1';
  v_user.user_phone      := '223aa3333';
  v_user.real_name       := 'xinyuan';
  v_user.identity_no     := '22222';
  v_user.user_email      := 'test@163.com';
  v_user.user_address    := 'hhhhhhh';
  v_user.user_birth_date := date '1986-01-01';
  v_user.user_status     := '1';
  v_user.user_remark     := 'sss';
  v_user.user_point      := 1000;
  v_user.register_date   := sysdate;

  save_user(v_user);
end;
/
select * from ma_users;

declare
  type test_record is record(
    user_name  ma_users.user_name%type,
    user_point ma_users.user_point%type);

  v_record test_record;
begin

  update ma_users t
     set t.user_point = t.user_point + 100
   where t.user_name = '幸运小子'
  returning t.user_name, t.user_point into v_record;
  dbms_output.put_line(v_record.user_name || '累计积分' || v_record.user_point);
end;
/

--事务管理

--commit:保存上一个commit或者rollback以来发生的所有变化,并且释放锁资源
--commit会释放会话中使用的任何行锁和表锁,比如使用SELECT FOR UPDATE添加的
--同时会把自上一个COMMIT或ROLLBACK语句以来创建的所有SAVEPOINT都清除
COMMIT ;
COMMIT WORK;
COMMIT COMMENT 'THIS IS A COMMENT';

--ROLLBACK 语句
--撤销从上一个commit或者rollback以来发生的所有变化,并且释放锁资源
rollback ;
rollback work;
rollback to savepoint_name;

--savepoint:创建一个保存点,有了保存点后可以进行部分回滚操作
savepoint savepoint_name;
--rollback回滚到某个保存点,这个保存点之后的改变全部撤销并释放资源
--不过在这点之前的改变以及锁仍然保留

--savepoin没有所谓作用范围一说

DECLARE
  test_savepoint_exp EXCEPTION;
  v_count number;
BEGIN

  update ma_users t
     set t.user_point = t.user_point + 100
   where t.user_name = '幸运小子';
   
  SAVEPOINT TEST_SAVEPOINT;
   update ma_users t
     set t.user_point = t.user_point + 100
   where t.user_name = '幸运小子';
  
  SAVEPOINT TEST_SAVEPOINT;
   update ma_users t
     set t.user_point = t.user_point + 100
   where t.user_name = '幸运小子';
  raise test_savepoint_exp;
exception
  when test_savepoint_exp then
    rollback to TEST_SAVEPOINT;
    select t.user_point
      into v_count
      from ma_users t
     where t.user_name = '幸运小子';
    dbms_output.put_line(v_count);
  
END;
/
--set transaction
--启动一个只读或者读写会话,构建一个隔离级别,或者为当前的事务分配一个专门的回滚段

set transaction read only 
--把当前事务定义成只读的,后续的查询看到的只是这个事务开始之前的已经提交的变化

set transaction read write 
--把当前事务定义成可读写的,并且这是缺省设置

set transaction isolation level serializable | read commit ;
--定义修改数据库的事务是如何处理的
--如果是serializable,则dml语句已经被另一个尚未提交的事务修改了,这个语句就会失败。
--这个命令要求数据库的初始化参数COMPATIBLE必须设置为7.3.0或者更高的值
--如果是READ COMMIT,一个dml语句请求的行级已经被另一个事务持有,则这个语句要一直等待锁被释放
--缺省行为

set transaction use rollback segment rollback_segname;
--为当前事务指定一个专门的回滚段,并把事务设置成可读写。不能和第一个命令一起使用

declare
  v_count number;
begin
  set transaction read only;
  select t.user_point
    into v_count
    from ma_users t
   where t.user_name = '幸运小子';
  dbms_output.put_line(v_count);

end;
/
 select t.user_point
    from ma_users t
   where t.user_name = '幸运小子';

--lock talbe
--用指定的模式锁定这个数据库表
lock table table_reference_list in lock_mode mode [nowait];
--lock mode
row share --行共享 允许其他用户同时更新其他行,允许其他用户同时加共享锁,不允许有独占(排他性质)的锁
row exclusive --行独占 行排他,允许其他用户同时更新其他行,只允许其他用户同时加行共享锁或者行排他锁
share --共享锁 不允许其他用户同时更新任何行,只允许其他用户同时加共享锁或者行共享锁
share row exclusive --共享行排他,允许其他用户同时更新其他行,只允许其他用户同时加行共享锁
exclusive --排他,其他用户禁止更新任何行,禁止其他用户同时加任何锁
--自治事务
--定义自治事务
--在声明单元加上以下语句
pragma autonomous_transaction;

--把一个块定义成自治事务,实际是把这个块中的DML语句和调用程序的事务环境完全的隔离开
--这个块就成为一个由其它事务启动的独立事务,前一个事务叫做主事务

--被定义程自治事务的块可以是
--最顶层的匿名块
--函数或者过程,或者在包里定义或者是一个独立的程序
--对象类型的方法(函数或者方法)
--数据库触发器


--自治事务的规则和限制
--如果自治事务的要访问的资源已经被主事务持有,程序就会发生死锁
--不能只用一个PRAGMA声明一个包中所有的子程序全部标识程自治的。必须对于包体中每个程序声明单元
--都明确指定自治事务

--如果想从一个已经执行了至少一个INSERT、update,merge,delete语句的自治事务程序没有任何
--错误的退出,必须明确的执行一个提交或者回滚

--commit和rollback语句只是结束了活动的自治事务,但不会终止自治例程。在一个自治块中
--可以使用多个commit或者ROLLBACK语句

--在一个自治事务中,不能回滚到主事务创建的SAVEPOINT
--自治事务提交后,对主事务可见
create or replace PROCEDURE UPDATE_USER(I_USER  IN VARCHAR2,
                                        i_point in number) IS
  PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
  UPDATE MA_USERS R
     SET R.USER_POINT = R.USER_POINT + i_point
   WHERE R.USER_NAME = I_USER;
    dbms_output.put_line('test1');
  COMMIT;
END UPDATE_USER;
 
DECLARE

BEGIN
  update ma_users t
     set t.user_point = t.user_point + 100
   where t.user_name = '幸运小子';
  UPDATE_USER('幸运小子2', 1000);
  dbms_output.put_line('test2');
END;
/
select * from ma_users
declare

  v_user ma_users%rowtype;
begin
  v_user.created_by      := 'system';
  v_user.created_date    := sysdate;
  v_user.updated_by      := 'system';
  v_user.updated_date    := sysdate;
  v_user.id_ma_users     := sys_guid();
  v_user.user_name       := '幸运小子1';
  v_user.user_password   := 'text123';
  v_user.user_sex        := '1';
  v_user.user_phone      := '233sss33';
  v_user.real_name       := 'xinyuan';
  v_user.identity_no     := '22222';
  v_user.user_email      := 'test@163.com';
  v_user.user_address    := 'hhhhhhh';
  v_user.user_birth_date := date '1986-01-01';
  v_user.user_status     := '1';
  v_user.user_remark     := 'sss';
  v_user.user_point      := 100;
  v_user.register_date   := sysdate;
  save_user(v_user);
  UPDATE_USER('幸运小子1', 1000);
  
end;
/
select * from ma_users

--自治事务的缺省行为是,只要在自治事务中执行了COMMIT或者ROLLBACK,这些改变立即对主事务可见


--什么时候使用自治事务

--自治事务的日志机制

------------------------------------------------------------------

1. 第三课作业中第二题的异常记录方法,大家可以完善下方法,将之改成支持自治事务的方法。
2. 这段程序中,ma_users是第一课作业建立的用户表,这段程序的目的是,一旦用户注册成功,在其默认积分基础上送1000积分。但程序里有BUG,请大家找出来,并且优化这个程序

create or replace procedure save_user(i_user ma_users%rowtype) is   
begin
  insert into ma_users values i_user;
exception
  when dup_val_on_index then
    update ma_users t
       set row = i_user
     where t.user_name = i_user.user_name;
end;
/
create or replace PROCEDURE UPDATE_USER(I_USER  IN VARCHAR2,
                                        i_point in number) IS
  PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
  UPDATE MA_USERS R
     SET R.USER_POINT = R.USER_POINT + i_point
   WHERE R.USER_NAME = I_USER;
    dbms_output.put_line('test1');
  COMMIT;
END UPDATE_USER;
declare
  v_user ma_users%rowtype;
begin
  v_user.created_by      := 'system';
  v_user.created_date    := sysdate;
  v_user.updated_by      := 'system';
  v_user.updated_date    := sysdate;
  v_user.id_ma_users     := sys_guid();
  v_user.user_name       := '幸运小子1';
  v_user.user_password   := 'text123';
  v_user.user_sex        := '1';
  v_user.user_phone      := '233sss33';
  v_user.real_name       := 'xinyuan';
  v_user.identity_no     := '22222';
  v_user.user_email      := 'test@163.com';
  v_user.user_address    := 'hhhhhhh';
  v_user.user_birth_date := date '1986-01-01';
  v_user.user_status     := '1';
  v_user.user_remark     := 'sss';
  v_user.user_point      := 100;
  v_user.register_date   := sysdate;
  save_user(v_user);
  UPDATE_USER('幸运小子1', 1000);
end;
/
3.【可选做】 这节讲到了MERGE的语句,请大家改写这个语句,实现如果存在则更新,不存在则插入的逻辑,注意PLSQL程序要有异常处理

--
1 修改成自治事务
PROCEDURE exception_logs_p (
      i_option_users        IN exception_logs.option_users%TYPE,
      i_method_name         IN exception_logs.method_name%TYPE,
      i_exception_line      IN exception_logs.exception_line%TYPE,
      i_exception_code      IN exception_logs.exception_code%TYPE,
      i_exception_message   IN exception_logs.exception_message%TYPE--i_exception_level    IN exception_logs.exception_level%TYPE
      )
   IS
      PRAGMA AUTONOMOUS_TRANSACTION;
      v_sysdate           DATE DEFAULT SYSDATE;
      v_exception_level   NUMBER DEFAULT 0;
 BEGIN     
  BEGIN
   SELECT   exception_level
      INTO v_exception_level
     FROM exception_level
      WHERE exception_code=i_exception_code;
      
   EXCEPTION 
   WHEN OTHERS THEN
    v_exception_level:=3;
    END ;
    
   BEGIN
      INSERT INTO exception_logs (option_users,
                                  method_name,
                                  exception_time,
                                  exception_line,
                                  exception_code,
                                  exception_message,
                                  exception_level)
           VALUES (i_option_users,
                   i_method_name,
                   v_sysdate,
                   i_exception_line,
                   i_exception_code,
                   i_exception_message,
                   v_exception_level);

      COMMIT;
   EXCEPTION
      WHEN OTHERS
      THEN
         ROLLBACK;
   END;
  END;
END exception_logs_pkg;
/

2 程序bug
1 由于UPDATE_USER过程使用了自治事务,这与主事务隔离开,主事务的insert没有提交,
导致UPDATE_USER没有获取到数据,导致数据更新错误
修改:1 可以在save_user上加上commit
2 取消UPDATE_USER的自治事务

3 

declare
  v_user ma_users%rowtype;
  v_usero varchar2(32) default user;
  v_erroeline varchar2(100);--not a number
  v_sqlcode number;
  v_sqlerrm varchar2(100);
begin
  v_user.created_by      := 'system';
  v_user.created_date    := sysdate;
  v_user.updated_by      := 'system';
  v_user.updated_date    := sysdate;
  v_user.id_ma_users     := sys_guid();
  v_user.user_name       := '乱世佳人12';
  v_user.user_password   := 'text123';
  v_user.user_sex        := '1';
  v_user.user_phone      := '233sss3311';
  v_user.real_name       := 'xinyuan';
  v_user.identity_no     := '22222';
  v_user.user_email      := 'test@163.com';
  v_user.user_address    := 'hhhhhhh';
  v_user.user_birth_date := date '1986-01-01';
  v_user.user_status     := '1';
  v_user.user_remark     := 'sss';
  v_user.user_point      := 100;
  v_user.register_date   := sysdate;
begin  
--register user
merge into MA_USERS m1
using( select count(id_ma_users) d from MA_USERS where user_name=v_user.user_name) m2
on (m2.d<>0)
--when matched then
 --update set m1.USER_POINT=m1.USER_POINT+1000,updated_date=sysdate 
 --where  user_name=v_user.user_name
when not matched then
 insert   values v_user ;
 --update point
 merge into MA_USERS m1
using( select m2.id_ma_users d from MA_USERS m2 where m2.id_ma_users=v_user.id_ma_users ) m2
on (m2.d=m1.id_ma_users)
when matched then
 update set m1.USER_POINT=m1.USER_POINT+1000,updated_date=sysdate ;
  dbms_output.put_line('v_user.id_ma_users='||v_user.id_ma_users);
 dbms_output.put_line('t1');
exception
 when others then
 v_erroeline:=dbms_utility.format_error_backtrace;
    v_sqlcode:=sqlcode;
    v_sqlerrm:=substr(SQLERRM,1,100);
  exception_logs_pkg.exception_logs_p (v_usero,'testerror',v_erroeline,v_sqlcode, v_sqlerrm); 
      RAISE; 
end;
commit;
end;
/
原文地址:https://www.cnblogs.com/yhq1314/p/10613204.html