【Oracle】游标

    科技2022-08-02  112

    目录

    我们为什么需要游标?游标简介游标分类与定义显式游标与隐式游标显式游标隐式游标 静态游标与动态游标 使用游标打开、关闭游标游标属性%ISOPEN%FOUND与%NOTFOUND%ROWCOUNT 提取数据单行提取批量提取 遍历游标LOOP循环WHILE循环FOR循环 修改游标数据 游标变量

    我们为什么需要游标?

      当我们在PL/SQL代码块中,对SELECT查询结果进行处理时,若结果只有一行,我们可以使用select into 语句来获取查询结果,以便在查询结果的基础上进行操作。

      但查询结果为多行记录时,是不可以使用select into语句的(若使用,会报错,所以现在也有人提出少使用select into语句,因为无法保证结果只有一行)。那么对于多行查询结果的处理,我们便缺少了方法,我们会很自然的想到用循环语句来遍历查询结果。但如何遍历呢?这时就需要使用PL/SQL中提供的游标机制了。

    游标简介

      若对C语言的指针熟悉,我们可以把游标和指针类比着看。游标就是指向select查询语句结果的内存块的一个指针。   游标在定义时,即使是显式定义,也不会立刻获取查询结果的数据,只有当游标被打开时,游标的查询语句才会被执行,然后将查询的结果保存到内存中。对打开的游标进行循环,像移动指针一样,来遍历获取查询结果。   打开的游标使用完毕后,需要进行关闭,否则查询结果一直保存于内存中,用户不断开连接,该块内存一直不会释放,占用资源,至使服务缓慢,甚至奔溃。

    游标分类与定义

    显式游标与隐式游标

      根据游标定义方式的不同,可以区分为显式游标与隐式游标。

    显式游标:使用关键字 CURSOR 来显式得定义一个游标,并在使用时需要先打开游标。隐式游标:由Oracle自动地为查询语句创建的隐式游标,隐式游标没有名称。

    显式游标

      在使用显式游标前,需要定义游标:为游标命名,并指定游标的select查询语句。定义语法如下:

    cursor 游标名 [ (入参) ] [return 返回类型] is 查询语句 [for update [of (一个或多个列名)] [nowait] ]

      其中被 [ ] 起来的均为可选项。最简单也是最常用的游标定义示例:

    declare cursor user_cursor is select * from sys_user r where r.dept_no = 1111; begin null; end;

      游标定义中,return用来限定游标的查询结果类型,一般为某张表的rowtype,如user%rowtype,有了return类型限制后,游标相对固定,但即使不限定也并不会影响使用,索引日常开发过程中并不会加上return。   游标在打开时也可以输入参数,来动态获取想要的游标数据,带入参的游标示例:

    declare cursor user_cursor_param (p_age in number,p_dept_no in number) is select * from sys_user r where r.age >= p_age and r.dept_no = p_dept_no; begin null; end;

      而for update则比较重要了,是游标使用中事务控制与锁的关键。有for update指定的游标,在被打开时,查询结果的数据会被锁定,不再允许其他事务对这些行数据进行DML操作,是行级锁。   当只想锁定行数据中某些字段时,接上of 字段名,多个字段间用 , 分隔开,即可实现字段锁。   当本事务想对这些数据上锁时,发现已经有人捷足先登,为已锁状态了,则本事务会一直等待,直到目标数据为未锁定作态才继续向下执行。而加上nowait则是不再等待,若已被上锁则头也不回地跳过~

    隐式游标

      在Oracle中,DML语句及select into语句被执行时,都会隐式得创建对应游标,并可通过SQL关键字来访问游标的属性,我们称这种Oracle自动创建的游标为隐式游标。   特别且常用的是,在for循环中,我们对查询结果的遍历时oracle可以自动帮我们做很多事情,甚至可以省去对游标的显式定义,这也是一种隐式游标,具体使用在下文游标操作中介绍。

    静态游标与动态游标

      由于有游标变量的存在,可以在使用时在为游标绑定具体查询语句,灵活性大大增强,我们称这种游标变量为动态游标。   而一般的显式游标是在声明时就指定了查询语句,不可改变,我们称这种游标为静态游标。

    使用游标

    打开、关闭游标

      当游标定以后,游标绑定的查询语句并未立马执行,而是需要等到我们打开游标时,才会执行。若游标包含FOR UPDATE语句,游标在打开前并不会锁住记录,游标打开后锁的影响才会反映出来,直到游标关闭。   打开游标的语法如下:

    OPEN 游标名 [ (入参) ];

      对游标声明的案例中的两个游标,打开示例为:

    OPEN user_cursor; OPEN user_cursor_param(1111,0);

      游标打开后,查询的结果集会放在运行内存中,当游标使用完成后应立即关闭,以释放资源。但若尝试关闭一个尚未打开的游标,将会抛出异常。关闭游标的语法如下:

    CLOSE 游标名;

    游标属性

      不论是显式游标还是隐式游标,都拥有四大游标属性:%ISOPEN、%FOUND、%NOTFOUND以及%ROWCOUNT。显式游标获取这些属性值时,需带上游标名作为前缀;隐式游标则使用SQL作为游标前缀。

    %ISOPEN

      %ISOPEN属性判断游标是否为已打开状态,若游标已打开则返回Ture,否则返回False,测试示例:

    declare cursor user_cursor is select * from sys_user r where r.dept_no = 1111; begin --声明游标后,尚未打开时 IF (user_cursor%ISOPEN) THEN dbms_output.put_line('游标已打开'); ELSE dbms_output.put_line('游标未打开'); END IF; --打开游标 OPEN user_cursor; IF (user_cursor%ISOPEN) THEN dbms_output.put_line('游标已打开'); ELSE dbms_output.put_line('游标未打开'); END IF; --将打开的游标关闭后 CLOSE user_cursor; IF (user_cursor%ISOPEN) THEN dbms_output.put_line('游标已打开'); ELSE dbms_output.put_line('游标未打开'); END IF; end; --------------------------------------- 输出结果为: 游标未打开 游标已打开 游标未打开

    %FOUND与%NOTFOUND

      %FOUND与%NOTFOUND的作用是检查是否从结果集中提取到了结果,游标打开前无法访问这两个属性,当游标打开后但尚未调用FETCH前,%FOUND和%NOTFOUND都会返回NULL,此后每成功获取下一行数据,%FOUND返回TRUE,而%NOTFOUND与之相反,返回FALSE。直到查询结果获取完毕时,无法继续获取下一行数据,%FOUND返回FALSE,%NOTFOUND返回TRUE。

    DECLARE user_row sys_user%ROWTYPE; CURSOR user_cursor is SELECT * FROM sys_user r WHERE r.dept_no = 1111; BEGIN OPEN user_cursor; IF user_cursor%FOUND IS NULL THEN dbms_output.put_line('游标打开后,未开始读取游标结果集,%FOUND为NULL'); END IF; IF user_cursor%NOTFOUND IS NULL THEN dbms_output.put_line('游标打开后,未开始读取游标结果集,%NOTFOUND为NULL'); END IF; LOOP FETCH user_cursor INTO user_row; EXIT WHEN user_cursor%NOTFOUND; -- 也可用%FOUND,写为: EXIT WHEN NOT user_cursor%FOUND; dbms_output.put_line('当前获取的数据USER_ID列为:' || user_row.user_id); END LOOP; CLOSE user_cursor; END; --------------------------------------- 输出结果为: 游标打开后,未开始读取游标结果集,%FOUND为NULL 游标打开后,未开始读取游标结果集,%NOTFOUND为NULL 当前获取的数据USER_ID列为:89757 当前获取的数据USER_ID列为:89758 ...

    %ROWCOUNT

      %ROWCOUNT属性用于返回目前已从游标中获取记录的行数,当游标打开时,其值为0,此后每从游标获取一行数据,其值加1。若游标未打开或已关闭,使用此属性会抛出异常。使用实例:

    DECLARE user_row sys_user%ROWTYPE; CURSOR user_cursor_param(p_dept_no IN NUMBER) IS SELECT * FROM sys_user r WHERE r.dept_no = p_dept_no; BEGIN OPEN user_cursor_param(11); dbms_output.put_line('当前行数为:' || user_cursor_param%ROWCOUNT); LOOP FETCH user_cursor_param INTO user_row; EXIT WHEN user_cursor_param%NOTFOUND; dbms_output.put_line('当前行数为:' || user_cursor_param%ROWCOUNT); END LOOP; CLOSE user_cursor_param; END; --------------------------------------- 输出结果为: 当前行数为:0 当前行数为:1 当前行数为:2 当前行数为:3 当前行数为:4 ...

    提取数据

      游标的数据,只有提取出来后才能被进一步使用,才能发挥数据的作用。使用FETCH语句,可以一行一行得提取,也可以使用BULK COLLECT子句一次性提取所有数据。

    单行提取

      使用FETCH INTO 语句,将游标的查询结果提取到应变量中,若查询结果为某些特定的字段,我们可以定义对应的变量来接收它们,示例:

    DECLARE user_name sys_user.user_name%TYPE; --可以直接定义为对应字段的类型 user_age NUMBER; --也可以使用对应的基础数据类型 CURSOR user_cursor_param(p_dept_no IN NUMBER) IS SELECT r.user_name, r.user_age FROM sys_user r WHERE r.dept_no = p_dept_no; BEGIN OPEN user_cursor_param(11); LOOP FETCH user_cursor_param INTO user_name, user_age; EXIT WHEN user_cursor_param%NOTFOUND; dbms_output.put_line('当前提取到的数据为:' || user_name || '---'|| user_age); END LOOP; CLOSE user_cursor_param; END;

      除了按逐个字段地提取,也可以使用%ROWTYPE类型的变量,提取整行数据:

    DECLARE user_row sys_user%ROWTYPE; CURSOR user_cursor_param(p_dept_no IN NUMBER) IS SELECT * FROM sys_user r WHERE r.dept_no = p_dept_no; BEGIN OPEN user_cursor_param(11); LOOP FETCH user_cursor_param INTO user_row; EXIT WHEN user_cursor_param%NOTFOUND; dbms_output.put_line('当前数据行的user_id为:' || user_row.user_id); END LOOP; CLOSE user_cursor_param; END;

    批量提取

      按字段提取 -> 按行提取,接下来就会想到,能不能直接按 ‘表’ 提取呢?自然就想到我们有表变量类型:TABLE OF。游标FETCH至表变量类型中,及可实现批量提取。示例:

    DECLARE TYPE user_table_type IS TABLE OF sys_user%ROWTYPE; user_table user_table_type; CURSOR user_cursor_param(p_dept_no IN NUMBER) IS SELECT * FROM sys_user r WHERE r.dept_no = p_dept_no; BEGIN OPEN user_cursor_param(11); FETCH user_cursor_param BULK COLLECT INTO user_table; CLOSE user_cursor_param; FOR i IN 1 .. user_table.count LOOP dbms_output.put_line( user_table(i).user_name ); END LOOP; END;

      使用FETCH BULK COLLECT INTO语句,可以直接将游标查询结果放入一张表变量中,此时立马关闭游标,也不会影响这个表变量的使用,数据依旧被保留下来了。

      还可以在FETCH语句最后加上LIMIT 行数,来实现一次性只获取固定行数的数据,并且不关闭游标时,下一次会接着上次的结果集继续向后获取。

    遍历游标

    LOOP循环

      在之前的案例中,其实已用到,LOOP循环的关键在于要有EXIT WHEN语句来控制循环结束的条件,不能造成死循环。且EXIT WHEN语句需要紧跟FETCH语句,否则虽然游标已获取完毕,但中间的业务处理语句会被多执行一次。示例:

    DECLARE user_row sys_user%ROWTYPE; CURSOR user_cursor_param(p_dept_no IN NUMBER) IS SELECT * FROM sys_user r WHERE r.dept_no = p_dept_no; BEGIN OPEN user_cursor_param(11); LOOP FETCH user_cursor_param INTO user_row; --dbms_output.put_line('当前行数为:' || user_cursor_param%ROWCOUNT); 若放在此处,则会多输出一次 EXIT WHEN user_cursor_param%NOTFOUND; dbms_output.put_line('当前行数为:' || user_cursor_param%ROWCOUNT); END LOOP; CLOSE user_cursor_param; END;

    WHILE循环

      对游标来说,WHILE循环与LOOP循环使用上没有特殊的区别,主要是两种循环本身的区别。WHILE循环并不总是会进入循环体,所以需要在循环前手动调用一次FECTH,或者加入OR %FOUND IS NULL 的判断。示例:

    DECLARE user_row sys_user%ROWTYPE; CURSOR user_cursor_param(p_dept_no IN NUMBER) IS SELECT * FROM sys_user r WHERE r.dept_no = p_dept_no; BEGIN OPEN user_cursor_param(11); --FETCH user_cursor_param INTO user_row; 或者在循环前FETCH一次,则不再需要OR %FOUND IS NULL条件 WHILE user_cursor_param%FOUND OR user_cursor_param%FOUND IS NULL LOOP dbms_output.put_line('当前行数为:' || user_cursor_param%ROWCOUNT); FETCH user_cursor_param INTO user_row; END LOOP; CLOSE user_cursor_param; END;

      为了防止业务逻辑被多执行一次,需要将业务逻辑放在FETCH语句之前。

    FOR循环

      FOR循环最特殊,也是在日常工作中对游标处理最为常用的遍历方式。因为它省去了很多代码,不需要打开游标,不需要定义游标变量,不需要关闭游标,不需要判断%FOUND或%NOTFOUND,甚至不需要声明游标。示例:

    BEGIN FOR user_cursor IN (SELECT * FROM sys_user r WHERE r.dept_no = 10) LOOP dbms_output.put_line(user_cursor.user_name); END LOOP; END;

      Oracle会隐式地为查询语句创建游标,并控制其打开与关闭,大大方便了遍历步骤,提升了开发效率。

    修改游标数据

      除了FOR UPDATE外,在操作游标数据时,还一个特别重要的语句:WHERE CURRENT OF 游标名来操作当前游标数据,而不需要取主键值定位的方式。示例:

    DECLARE user_row sys_user%ROWTYPE; CURSOR user_cursor_param(p_dept_no IN NUMBER) IS SELECT * FROM sys_user r WHERE r.dept_no = p_dept_no FOR UPDATE; BEGIN OPEN user_cursor_param(11); LOOP FETCH user_cursor_param INTO user_row; EXIT WHEN user_cursor_param%NOTFOUND; UPDATE sys_user r SET r.last_update_date = SYSDATE WHERE CURRENT OF user_cursor_param; END LOOP; CLOSE user_cursor_param; END;

      需要注意的是,使用WHERE CURRENT OF 游标名时,游标必须带有FOR UPDATE,否则会抛出异常。而其实日常开发工作中,我们更常使用主键的方式,来定位需要操作的数据,示例:

    DECLARE user_row sys_user%ROWTYPE; CURSOR user_cursor_param(p_dept_no IN NUMBER) IS SELECT * FROM sys_user r WHERE r.dept_no = p_dept_no; BEGIN OPEN user_cursor_param(11); LOOP FETCH user_cursor_param INTO user_row; EXIT WHEN user_cursor_param%NOTFOUND; UPDATE sys_user r SET r.last_update_date = SYSDATE WHERE r.user_id = user_row.user_id; --根据user_id主键来定位 END LOOP; CLOSE user_cursor_param; END;

    游标变量

      通常我们每定义一个游标,就为其绑定了查询语句,这种游标是静态的。而游标变量的存在,使得我们可以在定义时不绑定具体查询语句,而是在打开时绑定,更灵活且可重复绑定。   游标类型的定义与表变量类型的定义类似,定义语法如下:

    TYPE 类型名 IS REF CURSOR; 游标名 类型名;

      其中REF是Oracle中对指针类型的前缀关键之,更可证明游标的底层就是一个指针。

      而在使用游标变量时,需要在OPEN打开语句时,使用FOR关键字绑定游标的查询语句,打开后,游标的使用与一般游标无异。实例:

    DECLARE TYPE user_cur_type IS REF CURSOR; user_cur user_cur_type; user_row sys_user%ROWTYPE; dept_row sys_dept %ROWTYPE; BEGIN OPEN user_cur FOR SELECT * FROM sys_user r WHERE r.dept_no = 11; LOOP FETCH user_cur INTO user_row; EXIT WHEN user_cur%NOTFOUND; dbms_output.put_line(user_row.user_name); END LOOP; CLOSE user_cur; --关闭后,可以重新打开游标变量,并绑定新的查询语句,甚至是查询完全不同的表 OPEN user_cur FOR SELECT * FROM sys_dept r; LOOP FETCH user_cur INTO dept_row; EXIT WHEN user_cur%NOTFOUND; dbms_output.put_line(dept_row.dept_name); END LOOP; CLOSE user_cur; END;

      游标变量在工作中并不常用,我们常使用静态变量,静态为每一个游标赋值,避免出现以上实际查询sys_dept而游标名为user_cur不相符合的情况。

      在package存储过程中使用游标变量时,需将游标变量的定义放在包头中(即TYPE 类型名 IS REF CURSOR;这一段语句),在包体中则可以直接引用游标变量类型。

    Processed: 0.009, SQL: 8