Pembahasan kali ini mencakup topik-topik sebagai berikut:
* Apa itu Collection?
* Memilih Tipe Collection PL/SQL untuk Digunakan
* Mendefinisikan Tipe Collection
* Mendeklarasikan Variable-variable Collection PL/SQL
* Menginisialisasi dan Mereferensi Collections
* Memberikan Nilai Collections
* Menggunakan Collections PL/SQL bersama Perintah-perintah SQL
* Menggunakan Metode-metode Collection
* Menghindari Exception-exception Collection
* Menekan Overhead Perulangan untuk Collection dengan Bulk Binds
* Apa itu Records?
* Mendefinisikan dan Mendeklarasikan Records
* Menginisialisasi Records
* Memberikan Nilai Records
* Memanipulasi Records
5.1. Apa itu Collection?
* Index-by tables, juga dikenal sebagai associative arrays, mengijinkan kita mencari elemen-elemen menggunakan angka-angka dan string-string yang berubah-ubah untuk nilai subscript-nya. (Mereka mirip dengan hash table pada bahasa-bahasa pemrograman lain).
* Nested tables menyimpan angka yang berubah-ubah dari elemen-elemen. Mereka menggunakan angka-angka terurut sebagai subscript-subscriptnya. Kita dapat mendefinisikan tipe-tipe SQL yang ekuivalen, mengijinkan nested tables untuk disimpan di dalam table-table dalam database dan dimanipulasi melalui SQL.
* Varrays (kependekan dari variable-size arrays) menyimpan jumlah yang tetap dari elemen-elemen (meskipun kita dapat mengubah jumlah elemen-elemen saat runtime). Mereka menggunakan angka-angka urut sebagai subscript-subscript. Kita dapat mendefinisikan tipe-tipe SQL yang ekuivalen, dan mengijinkan varrays untuk disimpan pada table-table dalam database. Mereka dapat disimpan dan ditampilkan melalui SQL, namun kurang fleksibel dibandingkan dengan nested tables.
Meskipun collections hanya dapat memiliki satu dimensi, kita dapat memodelkan arrays multi-dimensi dengan menciptakan collections yang elemen-elemennya juga merupakan collections.
Untuk menggunakan collections dalam aplikasi, kita mendefinisikan satu atau lebih type-type PL/SQL, kemudian mendefinisikan variable-variable dari type-type tersebut. Kita dapat mendefinisikan collection dari type-type dalam procedure, function, atau package. Kita dapat melewatkan collection dari variable-variable sebagai parameter-parameter, untuk memindahkan data diantara aplikasi-aplikasi client-side dan sub-sub program.
Untuk mencari data yang lebih kompeks dari nilai-nilai tunggal, kita dapat menyimpan record-record PL/SQL atau object-object type SQL dalam collections. Nested tables dan varrays dapat juga menjadi atribut dari object-object type;
5.1.1. Memahami Nested Tables
Bersama dengan database, nested tables dapat dianggap table pada database dengan hanya satu kolom. Oracle menyimpan baris-baris data dari nested table dalam urutan-urutan tidak khusus. Namun, ketika kita menampilkan nested table ke dalam PL/SQL variable, baris-baris data diberikan subscript-subscript berurutan mulai dari 1. Hal ini memberikan kepada kita sebuah akses yang mirip array terhadap baris-baris data tunggal.
PL/SQL nested tables seperti array-array satu-dimensi. Kita dapat memodelkan array-array multi-dimensi dengan menciptakan nested tables yang elemen-elemennya juga berupa nested tables.
Nested tables berbeda dengan arrays dalam dua hal penting:
1. Arrays memiliki batas atas yang tetap, namun nested tables tidak memiliki batas atas (lihat Gambar 5-1). Sehingga, ukuran nested table dapat bertambah secara dinamis.
2. Arrays harus padat (memiliki subsrcipt-subscript urut). Sehingga, kita tidak dapat menghapus elemen-elemen di dalamnya.. Pada awalnya, nested tables juga padat, namun mereka dapat menjadi tipis (memiliki subsrcipt-subscript tidak urut). Sehingga, kita dapat menghapus elemen-elemen dari nested table menggunakan built-in procedure DELETE. Hal ini mungkin akan meninggalkan celah dalam index, namun built-in function NEXT mengijinkan kita untuk melakukan iterasi melalui serangkaian subscript-subscript.

Gambar 5-1 Arrays versus Nested Tables
5.1.2. Memahami Varrays
Item-item dengan tipe VARRAY disebut varrays. Mereka mengijinkan kita untuk menghubungkan identifier tunggal dengan seluruh collection. Hubungan ini mengijinkan kita untuk memanipulasi collection sebagai sebuah kesatuan dan mereferensi elemen-elemen tunggal secara mudah. Untuk mereferensi elemen, kita menggunakan sintaks standard subscripting (lihat Gambar 5-2). Sebagai contoh, Grade(3) mereferensi elemen ketiga dari varrays Grades.

Gambar 5-2 Varray berukuran 10
5.1.3. Memahami Associative Arrays (Index-By Tables)
Associative arrays merupakan kumpulan key-value berpasang-pasangan, yang mana setiap key adalah unik dan digunakan untuk mencari nilai yang terkait di dalam array. Key dapat berupa integer atau string.
Memberikan nilai menggunakan key untuk pertama kali menambahkan key tersebut kepada array terkait. Pemberian-pemberian nilai selanjutnya menggunakan key yang sama akan meng-update entry yang sama. Penting untuk memiliki key yang bernilai unik, baik dengan menggunakan primary key dari SQL table, atau dengan menggabungkan string-string secara bersama-sama untuk membentuk nilai unik.
Sebagai contoh, berikut ini adalah deklarasi dari tipe associative array, dan dua arrays dengan tipe tersebut, menggunakan key-key berupa string-string:
DECLARE
TYPE population_type IS TABLE OF NUMBER INDEX BY VARCHAR2(64);
country_population population_type;
continent_population population_type;
howmany NUMBER;
which VARCHAR2(64)
BEGIN
country_population('Greenland') := 100000;
country_population('Iceland') := 750000;
howmany := country_population('Greenland');
continent_population('Australia') := 30000000;
continent_population('Antarctica') := 1000; -- Creates new entry
continent_population('Antarctica') := 1001; -- Replaces previous
value
which := continent_population.FIRST; -- Returns 'Antarctica'
-- as that comes first alphabetically.
which := continent_population.LAST; -- Returns 'Australia'
howmany := continent_population(continent_population.LAST);
-- Returns the value corresponding to the last key, in this
-- case the population of Australia.
END;
/
Associative arrays membantu kit merepresentasikan kumpulan data dengan ukuran yang berubah-ubah, dengan pencarian cepat untuk elemen-elemen tunggal tanpa perlu mengetahui posisinya di dalam array dan tanpa harus mencari melalui seluruh elemen-elemen array. Hal ini seperti versi sederhana dari SQL table dimana kita dapat menampilkan nilai-nilai berdasarkan primary key. Untuk pencarian data dengan penyimpanan sementara secara sederhana, associative array mengijinkan kita untuk menghindari penggunaan space dan network operation yang dibutuhkan oleh SQL tables.
Karena associative arrays dimaksudkan untuk data sementara dibandingkan menyimpan data secara tetap, kita tidak dapat menggunakannya dengan perintah-perintah SQL seperti INSERT dan SELECT INTO. Kita dapat membuatnya persistent selama satu database session dengan mendeklarasikan tipe dalam package dan memberikan nilai-nilai dalam package body.
5.1.4. Bagaimana Globalization Settings Mempengaruhi Kunci-kunci VARCHAR2 untuk Associative Arrays
Jika pengaturan untuk national language atau globalization berubah selama session yang menggunakan associative arrays dengan nilai-nilai key VARCHAR2, program mungkin akan mengalami runtime error. Sebagai contoh, mengubah initialization paramters NLS_COMP atau NLS_SORT bersama dalam session mungkin menyebabkan method-metod seperti NEXT dan PRIOR untuk memunculkan exceptions. Jika kita perlu mengubah setting-setting ini selama session tertentu, yakinkan untuk mengaturnya kembali ke nilai-nilai asalnya sebelum melakukan operasi-operasi selanjutnya dengan bentuk-bentuk associative arrays seperti ini.
Ketika kita mendeklarasikan associative array menggunakan string sebagai key, deklarasi harus menggunakan tipe VARCHAR2, STRING, atau LONG. Kita dapat menggunakan tipe berbeda, seperti NCHAR atau NVARCHAR2, sebagai kunci untuk mereferensi associative array. Kita bahkan dapat menggunakan tipe seperti DATE, sepanjang ia dapat dikonversi ke VARCHAR2 dengan function TO_CHAR.
Namun, kita harus berhati-hati ketika menggunakan tipe-tipe lain dimana nilai-nilai yang digunakannya sebagai key adalah konsisten dan unik. Sebagai contoh, nilai string SYSDATE mungkin berubah jika initialization parameter NLS_DATE_FORMAT berubah, sehingga array_element(SYSDATE) tidak akan menghasilkan hasil yang sama seperti sebelumnya. Dua nilai NVARCHAR2 yang berbeda dapat diubah ke nilai VARCHAR2 yang sama (berisi tanda tanya kecuali karakter-karakter national tertentu). Dalam kasus ini, array_element(national_string1) dan array_element(national_string2) mungkin akan mengacu kepada elemen yang sama.
Ketika kita melewatkan associative array sebagai parameter ke remote database dengan menggunakan database link, kedua database dapat memiliki globalization settings yang berbeda. Ketika remote database melakukan operasi-operasi seperti FIRST dan NEXT, ia menggunakan urutan karakternya sendiri bahkan jika terdapat bentuk yang berbeda dari urutan dimana collection berasal. Jika perbedaan-perbedaan character set berarti bahwa dua key yang unik adalah tidak unik pada remote database, program akan mendapati exception VALUE_ERROR.
5.2. Memilih Tipe-tipe Collection PL/SQL yang Digunakan
Jika kita telah memiliki kode atau business logic yang menggunakan beberapa bahasa pemrograman lain, kita biasanya dapat menterjemahkan array dari bahasa tersebut dan mengatur types secara langsung ke tipe-tipe collection PL/SQL.
* Arrays dalam bahasa lain menjadi VARRAYs dalam PL/SQL
* Sets dan bags dalam bahasa-bahasa lain menjadi nested tables (table bersarang) dalam PL/SQL
* Hash tables dan bentuk-bentuk lain dari pencarian table tak berurut dalam bahasa-bahasa lain menjadi associative arrays dalam PL/SQL
Ketika kita menulis kode asli atau mendesain business logic mulai awal, kita harus mempertimbangkan kekuatan-kekuatan dari setiap collection type untuk memutuskan mana yang tepat untuk setiap situasi.
5.2.1. Memilih Antara Nested Tables dan Associative Arrays
Nested tables dan associative arrays (sebelumnya dikenal sebagai index-by tables) menggunakan notasi subscript yang mirip, namun mereka memiliki karakteristik-karakteristik berbeda ketika ia datang ke parameter passing yang persistent dan ringan.
Nested tables dapat disimpan dalam kolom, namun associative arrays tidak. Nested tables tepat untuk hubungan-hubungan data yang penting yang harus disimpan secara tetap (persisten).
Associative arrays tepat untuk table-table pencarian kecil relatif dimana collection dapat dibangun dalam memori setiap kali procedure dipanggil atau package diinisialisasi. Mereka baik untuk mengumpulkan informasi yang volumenya tidak diketahui sebelumnya, karena tidak ada limit tetap pada ukurannya. Nilai-nilai index mereka lebih fleksibel, karena subscript-subscript associative arrays dapat bernilai negatif, dapat tidak terurut, dan dapat menggunakan nilai-nilai string dibanding kan angka-angka ketika hal itu lebih tepat.
PL/SQL secara otomatis mengkonversi antara host arrays dan associative arrays yang menggunakan nilai-nilai key numerik. Jalan paling efisien untuk melewatkan collections ke dan dari database server adalah menggunakan anonymous PL/SQL blocks untuk mengikat input dan output secara borongan dari host arrays ke associative arrays.
5.2.2. Memilih Antara Nested Tables dan Varrays
Varrays adalah pilihan yang baik ketika jumlah elemen-elemen tidak diketahui, dan ketika elemen-elemen biasanya seluruhnya diakses secara terurut. Ketika disimpan di database, varrays tetap menggunakan urutan dan subscript-subscriptnya.
Setiap varray disimpan sebagai objek tunggal, baik di dalam table pada kolom (jika varray kurang dari 4KB) atau di luar table namun tetap di dalam tablespace yang sama (jika varray lebih besar dari 4KB). Kita harus meng-update atau menampilkan seluruh elemen-elemen dari varray pada saat yang sama, yang mana lebih tepat ketika melakukan beberapa operasi pada seluruh elemen-elemen sebanyak sekali. Namun kita mungkin mendapatinya tidak berfungsi untuk menyimpan dan menampilkan jumlah-jumlah elemen-elemen yang besar dengan cara ini.
Nested tables dapat menjadi jarang: kita dapat menghapus elemen-elemen yang berubah-ubah, dibandingkan hanya menghapus item dari akhirnya. Data pada nested table disimpan out-of-line di dalam store table, sebuah table yang di-generate oleh sistem, yang terkait dengan nested table tersebut. Hal ini membuat nested table tepat untuk query dan update yang hanya berefek pada beberapa elemen-elemen dari collection. Kita tidak dapat mempercayakan order dan subscript-subscript dari nested table yang telah stabil seperti halnya table disimpan dan ditampilkan, karena order dan subscript-subscript tidak dilindungi ketika nested table disimpan di dalam database.
5.3. Mendefinisikan Tipe-tipe Collection
Untuk menciptakan collections, kita mendefinisikan tipe collection, kemudian mendeklarasikan variables dari tipe tersebut. Kita dapat mendefinisikan tipe-tipe TABLE dan VARRAY di dalam bagian declarative pada sebuah blok PL/SQL, subprogram, atau package.
Collections mengikuti aturan-aturan scoping dan instantiation seperti tipe-tipe dan variable-variable lain. Dalam sebuah blok atau subprogram, collections dibentuk ketika kita masuk ke blok atau subprogram dan berhenti tampil ketika kita keluar. Dalam package, collections ada ketika kita pertama kali mereferensi package dan berhenti ada ketika kita mengakhiri session database.
Nested Tables
Untuk nested tables, kita gunakan sintaks
TYPE type_name IS TABLE OF element_type [NOT NULL];
type_name adalah penentu tipe yang digunakan sebelumnya untuk mendeklarasikan collections. Untuk nested tables yang dideklarasikan bersama PL/SQL, element_type merupakan beberapa tipe data PL/SQL, kecuali:
REF CURSOR
Nested tables yang dideklarasikan secara global di dalam SQL memiliki batasan-batasan pada tipe-tipe elemennya. Mereka tidak dapat menggunakan tipe-tipe elemen berikut ini:
BINARY_INTEGER, PLS_INTEGER
BOOLEAN
LONG, LONG RAW
NATURAL, NATURALN
POSITIVE, POSITIVEN
REF CURSOR
SIGNTYPE
STRING
Varrays
Untuk varrays, gunakan sintaks:
TYPE type_name IS {VARRAY | VARYING ARRAY} (size_limit)
OF element_type [NOT NULL];
Arti dari type_name dan element_type adalah sama seperti untuk nested tables.
size_limit merupakan literal integer positif yang merepresentasikan jumlah maksimum elemen dalam array. Ketika mendefinisikan tipe VARRAY, kita harus menentukan nilai maksimumnya. Dalam contoh berikut ini, kita mendefinisikan tipe yang dapat menampung hingga 366 tanggal:
DECLARE
TYPE Calendar IS VARRAY(366) OF DATE;
Associative Arrays
Untuk associative arrays (dikenal juga dengan index-by tables), gunakan sintaks:
TYPE type_name IS TABLE OF element_type [NOT NULL]
INDEX BY [BINARY_INTEGER | PLS_INTEGER | VARCHAR2(size_limit)];
INDEX BY key_type;
Sebuah key_type dapat berupa numerik, baik BINARY_INTEGER atau PLS_INTEGER. Ia dapat juga berupa VARCHAR2 atau salah satu dari subtipe-nya VARCHAR, STRING, atau LONG. Kita harus menentukan panjang dari keys berbasis VARCHAR2, kecuali LONG dimana ekuivalen dengan jika kita mendeklarasikan tipe key dari VARCHAR2(32760). Tipe-tipe RAW, LONG RAW, ROWID, CHAR, dan CHARACTER tidak diperbolehkan sebagai keys untuk sebuah associative array.
Klausa inisialisasi tidak diperlukan (atau diperbolehkan).
Ketika kita mereferensi elemen dari associative array yang menggunakan key berbasis VARCHAR2, kita dapat menggunakan tipe-tipe lain, seperti DATE atau TIMESTAMP, sejauh mereka dapat dikonversi ke VARCHAR2 dengan function TO_CHAR.
Index-by tables dapat menyimpan data menggunakan nilai primary key sebagai index, dimana nilai-nilai key tidak sekuensial (terurut). Dalam contoh di bawah ini, kita menyimpan record tunggal di dalam index-by table, dan subscript-nya adalah 7468, bukannya 1.
DECLARE
TYPE EmpTabTyp IS TABLE OF emp%ROWTYPE
INDEX BY BINARY_INTEGER;
emp_tab EmpTabTyp;
BEGIN
/* Retrieve employee record. */
SELECT * INTO emp_tab(7468) FROM emp WHERE empno = 7468;
END;
5.3.1. Mendefinisikan Tipe-tipe SQL yang Ekuivalen dengan Tipe-tipe Collection PL/SQL
Untuk menyimpan nested tables dan varrays di dalam table-table database, kita juga harus mendeklarasikan tipe-tipe SQL menggunakan perintah CREATE TYPE. Tipe-tipe SQL dapat digunakan sebagai kolom-kolom atau atribut-atribut dari tipe-tipe objek SQL.
Kita dapat mendeklarasikan tipe-tipe yang ekuivalen di dalam PL/SQL, atau menggunakan nama tipe SQL di dalam deklarasi variable PL/SQL.
Contoh Nested Table
Script SQL*Plus berikut ini menunjukkan bagaimana kita mungkin mendeklarasikan nested table di dalam SQL, dan menggunakannya sebagai atribut dari tipe objek:
CREATE TYPE CourseList AS TABLE OF VARCHAR2(10) -- define type
/
CREATE TYPE Student AS OBJECT ( -- create object
id_num INTEGER(4),
name VARCHAR2(25),
address VARCHAR2(35),
status CHAR(2),
courses CourseList) -- declare nested table as attribute
/
Indentifier courses merepresentasikan seluruh nested tables. Setiap elemen dari courses akan menyimpan code name dari college course seperti ‘Math 1020′.
Contoh Varray
Script di bawah ini menciptakan kolom database yang menyimpan varrays. Setiap elemen varray mengandung sebuah VARCHAR2.
-- Each project has a 16-character code name.
-- We will store up to 50 projects at a time in a database column.
CREATE TYPE ProjectList AS VARRAY(50) OF VARCHAR2(16);
/
CREATE TABLE department ( -- create database table
dept_id NUMBER(2),
name VARCHAR2(15),
budget NUMBER(11,2),
-- Each department can have up to 50 projects.
projects ProjectList)
/
5.4. Mendeklarasikan Variable-variable Collection PL/SQL
Sekali kita mendefinisikan tipe collection, kita dapat mendeklarasikan variable dari tipe tersebut. Kita menggunakan nama tipe baru di dalam deklarasi, sama dengan tipe-tipe predefined seperti NUMBER dan INTEGER.
Contoh: Mendeklarasikan Nested Tables, Varrays, dan Associative Arrays
DECLARE
TYPE nested_type IS TABLE OF VARCHAR2(20);
TYPE varray_type IS VARRAY(50) OF INTEGER;
TYPE associative_array_type IS TABLE OF NUMBER
INDEXED BY BINARY_INTEGER;
v1 nested_type;
v2 varray_type;
v3 associative_array_type;
Contoh %TYPE
Kita dapat menggunakan %TYPE untuk menentukan tipe data dari collection yang telah didefinisikan sebelumnya, sehingga perubahan definisi dari collection secara otomatis akan mengubah variable-variable lain yang bergantung kepada jumlah elemen-elemen atau tipe elemen:
DECLARE
TYPE Platoon IS VARRAY(20) OF Soldier;
p1 Platoon;
-- Jika kita mengubah jumlah soldiers di dalam platoon, p2 akan
-- merefleksikan perubahan tersebut ketika blok ini di-recompile.
p2 p1%TYPE;
Contoh: Mendeklarasikan Parameter Procedure sebagai Nested Table
Kita dapat mendefinisikan collection-collection sebagai parameter-parameter formal dari functions dan procedures. Dengan cara itu, ktia dapat melewatkan collection-collection ke stored subprograms dan dari satu subprogram ke lainnya. Contoh berikut ini mendeklarasikan nested table sebagai parameter dari procedure ter-package:
CREATE PACKAGE personnel AS
TYPE Staff IS TABLE OF Employee;
...
PROCEDURE award_bonuses (members IN Staff);
END personnel;
Untuk memanggil PERSONEL.AWARD_BONUSES dari luar package, kita mendeklarasikan variable dari tipe PERSONEL.STAFF dan melewatkan variable tersebut sebagai parameter.
Kita dapat juga menentukan tipe collection di dalam klausa RETURN dari spesifikasi function:
DECLARE
TYPE SalesForce IS VARRAY(25) OF Salesperson;
FUNCTION top_performers (n INTEGER) RETURN SalesForce IS ...
Contoh: Menentukan Tipe-tipe Elemen Collection dengan %TYPE dan %ROWTYPE
Untuk menentukan tipe elemen, kita menggunakan %TYPE, yang menyediakan tipe data dari variable atau kolom pada database. Juga, kita dapat menggunakan %ROWTYPE, yang menyediakan tipe baris data dari cursor atau table pada database. Dua contohnya adalah sebagai berikut:
DECLARE
TYPE EmpList IS TABLE OF emp.ename%TYPE; -- berdasar pada column
CURSOR c1 IS SELECT * FROM dept;
TYPE DeptFile IS VARRAY(20) OF c1%ROWTYPE; -- berdasar pada cursor
Contoh: VARRAY dari Records
Dalam contoh berikut ini, kita menggunakan tipe RECORD untuk menentukan tipe elemen:
DECLARE
TYPE AnEntry IS RECORD (
term VARCHAR2(20),
meaning VARCHAR2(200));
TYPE Glossary IS VARRAY(250) OF AnEntry;
Contoh: Constraint NOT NULL pada Elemen-elemen Collection
Kita dapat juga memaksakan constraint NOT NULL pada tipe elemen:
DECLARE
TYPE EmpList IS TABLE OF emp.empno%TYPE NOT NULL;
5.5. Menginisialisasi dan Mereferensi Collections
Sampai kita menginisialisasinya, nested table atau varray secara atomic adalah null: collection itu sendiri adalah null, bukan elemen-elemennya. Untuk menginisialisasi nested table atau varray, kita menggunakan constructor, sebuah function system-defined dengan nama yang sama seperti tipe collection. Function ini “membangun” collections dari elemen-elemen yang dilewatkan kepadanya.
Kita harus secara eksplisit memanggil constructor untuk setiap varray dan variable nested table. (Associative arrays, bentuk ketiga dari collection, tidak menggunakan constructors). Pemanggilan constructor diperbolehkan dimanapun pemanggilan function diperbolehkan.
Contoh: Constructor untuk Nested Table
Dalam contoh berikut ini, kita melewatkan banyak elemen-elemen ke constructor CounterList(), yang mengembalikan nested table yang mengandung elemen-elemen tersebut:
DECLARE
TYPE CourseList IS TABLE OF VARCHAR2(16);
my_courses CourseList;
BEGIN
my_courses :=
CourseList('Econ 2010', 'Acct 3401', 'Mgmt 3100');
END;
Dikarenakan nested table tidak memiliki ukuran maksimum yang dideklarasikan, kita dapat meletakkan elemen-elemen di dalam constructor sebanyak yang dibutuhkan.
Contoh: Constructor untuk Varray
Dalam contoh berikutnya, kita melewatkan tiga objek ke constructor ProjectList(), yang menghasilkan varray berisi objek-objek tersebut.
DECLARE
TYPE ProjectList IS VARRAY(50) OF VARCHAR2(16);
accounting_projects ProjectList;
BEGIN
accounting_projects :=
ProjectList('Expense Report, 'Outsourcing', 'Auditing);
END;
Kita tidak perlu menginisialisasi seluruh varray. Sebagai contoh, jika varray memiliki ukuran maksimum 50, kita dapat melewatkan kurang dari 50 elemen kepada constructor-nya.
Contoh: Collection Constructor Termasuk Elemen-elemen null
Kecuali kita menentukan constraint NOT NULL, kita dapat melewatkan elemen-elemen null ke constructor. Contohnya adalah sebagai berikut:
BEGIN
my_courses := CourseList('Math 3010', NULL, 'Stat 3202');
Contoh: Mengkombinasikan Deklarasi Collection dan Constructor
Kita dapat menginisialisasi collection di dalam deklarasinya, yang mana merupakan praktek pemrograman yang baik:
DECLARE
TYPE CourseList IS TABLE OF VARCHAR2(16);
my_courses CourseList :=
CourseList('Art 1111', 'Hist 3100', 'Engl 2005');
Contoh: Constructor Varray Kosong
Jika kita memanggil constructor tanpa argumen-argumen, kita akan mendapatkan collection kosong namun tidak null:
DECLARE
TYPE Clientele IS VARRAY(100) OF Customer;
vips Clientele := Clientele(); -- initialize empty varray
BEGIN
IF vips IS NOT NULL THEN -- condition yields TRUE
...
END IF;
END;
Dalam kasus ini, kita dapat memanggil method EXTEND dari collection untuk menambahkan elemen-elemen kemudian.
Contoh: Constructor Nested Table Dalam Perintah SQL
Dalam contoh kali ini, kita menambahkan beberapa nilai-nilai scalar dan nested table CourseList ke dalam table SOPHOMORES.
BEGIN
INSERT INTO sophomores
VALUES (5035, 'Janet Alvarez', '122 Broad St', 'FT',CourseList('Econ 2010', 'Acct 3401', 'Mgmt 3100'));
Contoh: Constructor Varray Dalam Perintah SQL
Dalam contoh berikut ini, kita menambahkan data ke dalam table DEPARTMENT. Constructor varray ProjectList() menyediakan nilai untuk kolom PROJECTS.
BEGIN
INSERT INTO department
VALUES(60, 'Security', 750400,
ProjectList('New Badges', 'Track Computers', 'Check Exits'));
5.5.1. Mereferensi Elemen-elemen Collection
Setiap referensi ke elemen mengandung nama collection dan subscript dalam tanda kurung. Subscript menentukan elemen mana yang diproses. Untuk mereferensi elemen, kita menentukan subscript-nya menggunakan sintaks:
collection_name(subscript)
dimana subscript merupakan ekspresi yang menghasilkan integer dalam banyak kasus, atau VARCHAR2 untuk associative arrays yang dideklarasikan dengan string-string sebagai keys (kunci).
Jangkauan yang diperbolehkan untuk subscript adalah:
* Untuk nested tables, 1..2**31.
* Untuk varrays, 1..size_limit, dimana kita menentukan batas di dalam deklarasi.
* Untuk associative arrays dengan kunci numerik (numeric key), -2**31..2**31.
* Untuk associative arrays dengan kunci string (string key), panjang dari kunci dan jumlah nilai-nilai yang mungkin bergantung pada batas panjang VARCHAR2 di dalam deklarasi tipe, dan database character set.
Contoh: Mereferensi Elemen Nested Table dengan Subscript
Contoh berikut ini menunjukkan bagaimana mereferensi elemen di dalam nested table NAMES:
DECLARE
TYPE Roster IS TABLE OF VARCHAR2(15);
names Roster := Roster('J Hamil', 'D Caruso', 'R Singh');
BEGIN
FOR i IN names.FIRST .. names.LAST
LOOP
IF names(i) = 'J Hamil' THEN
NULL;
END IF;
END LOOP;
END;
Contoh: Melewatkan Elemen Nested Table sebagai Parameter
Contoh berikut ini menunjukkan bahwa kita dapat mereferensi elemen-elemen dari collection di dalam pemanggilan-pemanggilan subprogram:
DECLARE
TYPE Roster IS TABLE OF VARCHAR2(15);
names Roster := Roster('J Hamil', 'D Piro', 'R Singh');
i BINARY_INTEGER := 2;
BEGIN
verify_name(names(i)); -- call procedure
END;
5.6. Memberikan Nilai Collections
Sebuah collection dapat diberikan kepada lainnya dengan perintah INSERT, UPDATE, FETCH, atau SELECT, perintah-perintah pemberian nilai (assignment statement), atau pemanggilan subprogram.
Kita dapat memberikan nilai dari ekspresi ke elemen tertentu di dalam collection dengan menggunakan sintaks:collection_name(subscript):= expression;
dimana expression menghasilkan nilai dari tipe yang telah ditentukan untuk elemen-elemen di dalam definisi collection type.
Contoh: Kompatibilitas Tipe Data
Contoh berikut ini menunjukkan bahwa collections harus memiliki tipe data yang sama agar pemberian nilai dapat bekerja. Memiliki tipe elemen yang sama tidaklah cukup.
DECLARE
TYPE Clientele IS VARRAY(100) OF Customer;
TYPE Vips IS VARRAY(100) OF Customer;
-- Dua variable pertama ini memiliki tipe data yang sama.
group1 Clientele := Clientele(...);
group2 Clientele := Clientele(...);
-- Variable ketiga ini memiliki deklarasi yang mirip,
-- namun tidak memiliki tipe yang sama.
group3 Vips := Vips(...);
BEGIN
-- Diijinkan karena mereka memiliki tipe data yang sama
group2 := group1;
-- Tidak diijinkan karena mereka memiliki tipe data yang berbeda
group3 := group2;
END;
Contoh: Memberikan Nilai Null ke Nested Table
Kita memberikan nilai nested table null secara atomik atau varray untuk nested table atau varray kedua. Dalam kasus ini, collection kedua harus diinisialisasi ulang:
DECLARE
TYPE Clientele IS TABLE OF VARCHAR2(64);
-- Nested table ini memiliki beberapa nilai-nilai.
group1 Clientele := Clientele('Customer 1','Customer 2');
-- Nested table tidak diinisialisasi ("atomically null").
group2 Clientele;
BEGIN
-- Pertama, pengecekan IF group1 IS NULL menghasilkan FALSE.
-- Lalu kita memberikan nested table null kepada group1.
group1 := group2;
-- Sekarang, pengecekan IF group1 IS NULL menghasilkan TRUE.
-- Kita harus menggunakan constructor lain untuk memberikan nilai kepadanya.
END;
Dengan cara yang sama, memberikan nilai NULL kepada collection membuatnya null secara atomik.
Contoh: Eksepsi-eksepsi yang Mungkin untuk Pemberian Nilai Collection
Memberikan nilai kepada elemen collection dapat menyebabkan banyak eksepsi (exception):
* Jika subscript null atau tidak dapat dikonversi ke tipe data yang tepat, PL/SQL memunculkan predefined exception VALUE_ERROR. Biasanya, subscript haruslah integer. Associative arrays dapat juga dideklarasikan untuk memiliki subscript-subscript VARCHAR2.
* Jika subscript mereferensi ke elemen yang tidak terinisialisasi, PL/SQL memunculkan SUBSCRIPT_BEYOND_COUNT.
* Jika collection secara atomik adalah null, PL/SQL memunculkan COLLECTION_IS_NULL.
DECLARE
TYPE WordList IS TABLE OF VARCHAR2(5);
words WordList;
BEGIN
/* Assume execution continues despite the raised exceptions. */
-- Memunculkan COLLECTION_IS_NULL. Kita belum menggunakan constructor.
-- Eksepsi ini diberlakukan untuk varrays dan nested tables, namun tidak
-- associative arrays yang mana tidak memerlukan constructor.
words(1) := 10;
-- Setelah menggunakan constructor, kita dapat memberikan nilai ke elemen-elemen.
words := WordList(10,20,30);
-- Suatu ekspresi yang menghasilkan VARCHAR2(5) adalah OK.
words(1) := 'yes';
words(2) := words(1) || 'no';
-- Memunculkan VALUE_ERROR karena nilai yang diberikan terlalu panjang.
words(3) := 'longer than 5 characters';
-- Memunculkan VALUE_ERROR karena subscript dari nested table harus integer
words('B') := 'dunno';
-- Memunculkan SUBSCRIPT_BEYOND_COUNT karena kita hanya membuat 3 elemen
-- di dalam constructor. Untuk menambahkan yang baru, kita harus memanggil method EXTEND terlebih dahulu
words(4) := 'maybe';
END;
5.7. Membandingkan Collection
Kita dapat memeriksa apakah sebuah collection null, namun tidak untuk memeriksa apakah dua buah collection sama atau tidak. Kondisi-kondisi seperti lebih besar dari, lebih kecil dari, dan seterusnya juga tidak diperbolehkan.
Contoh: Memeriksa Apakah Collection Null
Nested tables dan varrays dapat secara atomik null, sehingga mereka dapat diperiksa apakah null atau tidak:
DECLARE
TYPE Staff IS TABLE OF Employee;
members Staff;
BEGIN
-- Kondisi menghasilkan TRUE karena kita tidak belum menggunakan constructor.
IF members IS NULL THEN ...
END;
Contoh: Membandingkan Dua Collection
Collections tidak dapat secara langsung dibandingkan untuk kesamaan atau ketidaksamaannya. Sebagai contoh, kondisi IF berikut ini tidak diperbolehkan:
DECLARE
TYPE Clientele IS TABLE OF VARCHAR2(64);
group1 Clientele := Clientele('Customer 1', 'Customer 2');
group2 Clientele := Clientele('Customer 1', 'Customer 3');
BEGIN
-- Pengecekan kesamaan menyebabkan compilation error.
IF group1 = group2 THEN
...
END IF;
END;
Batasan ini juga berlaku untuk perbandingan-perbandingan implisit. Sebagai contoh, collections tidak dapat nampak di dalam daftar DISTINCT, GROUP BY, atau ORDER BY.
Jika kita ingin melakukan operasi-operasi perbandingan seperti itu, kita harus mendefinisikan gagasan kita sendiri mengenai artinya bagi collection untuk sama dengan atau lebih besar dari, lebih kecil dari, dan seterusnya, dan menulis satu atau lebih function untuk menguji collections dan elemen-elemennya dan menghasilkan nilai true atau false.
5.8. Menggunakan Collections PL/SQL dengan Perintah-perintah SQL
Collections mengijinkan kita memanipulasi tipe-tipe data kompleks di dalam PL/SQL. Program kita dapat menghitung subscript-subscript untuk memproses elemen-elemen tertentu di dalam memory, dan menggunakan SQL untuk menyimpan hasil-hasilnya di dalam database tables.
Contoh: Menciptakan Tipe SQL Berkaitan dengan Nested Table PL/SQL
Di dalam SQL*Plus, kita dapat menciptakan tipe-tipe SQL yang definisinya terkait dengan nested tables dan varrays PL/SQL:
CREATE TYPE CourseList AS TABLE OF VARCHAR2(64);
Kita dapat menggunakan tipe-tipe SQL ini sebagai kolom-kolom di dalam database tables:
CREATE TABLE department (
name VARCHAR2(20),
director VARCHAR2(20),
office VARCHAR2(20),
courses CourseList)
NESTED TABLE courses STORE AS courses_tab;
Setiap item di dalam kolom COURSES merupakan nested table yang akan menyimpan kursus yang ditawarkan oleh departemen tertentu. Klausa NESTED TABLE diperlukan ketika database table memiliki kolom nested table. Klausa tersebut mengidentifikasi nested table dan memberikan nama table-table yang di-generate oleh sistem, dimana Oracle menyimpan data nested table.
Contoh: Menambahkan Nested Table ke Database Table
Sekarang, kita dapat mempopulasikan database table. Constructor table menyediakan nilai-nilai yang seluruhnya akan menuju ke kolom tunggal COURSES:
BEGIN
INSERT INTO department VALUES('English','Lynn Saunders', 'Breakstone Hall 205',CourseList('Expository Writing','Film and Literature','Modern Science Fiction','Discursive Writing','Modern English Grammar','Introduction to Shakespeare','Modern Drama','The Short Story','The American Novel'));
END;
Contoh: Menampilkan Nested Table PL/SQL dari Database Table
Kita dapat menampilkan seluruh kursus yang ditawarkan oleh departemen English ke dalam nested table PL/SQL:
DECLARE
english_courses CourseList;
BEGIN
SELECT courses
INTO english_courses
FROM department
WHERE name = 'English';
END;
Di dalam PL/SQL, kita dapat memanipulasi nested table dengan melakukan looping melalui elemen-elemennya, menggunakan method-method seperti TRIM atau EXTEND, dan mengubah beberapa atau seluruh data dari elemen-elemen tersebut. Setelah itu, kita dapat menyimpan table yang telah di-update tersebut ke dalam database lagi.
Contoh: Mengupdate Nested Table di dalam Database Table
Kita dapat merevisi daftar kursus yang ditawarkan oleh departemen English:
DECLARE
new_courses CourseList := CourseList('Expository Writing', 'Film and Literature', 'Discursive Writing', 'Modern English Grammar', 'Realism and Naturalism', 'Introduction to Shakespeare', 'Modern Drama', 'The Short Story', 'The American Novel', '20th-Century Poetry', 'Advanced Workshop in Poetry');
BEGIN
UPDATE department
SET courses = new_courses
WHERE name = 'English';
END;
5.8.1. Beberapa Contoh Varray
Dalam SQL*Plus, asumsikan kita mendefinisikan object type Project, seperti berikut ini:
CREATE TYPE Project AS OBJECT (
project_no NUMBER(2), title VARCHAR2(35),
cost NUMBER(7,2));
Berikutnya, kita mendefinisikan tipe VARRAY ProjectList, yang menyimpan objek-objek Project:
CREATE TYPE ProjectList AS VARRAY(50) OF Project;
Akhirnya, kita menciptakan relational table department, yang memiliki kolom dengan tipe ProjectList, seperti contoh berikut:
CREATE TABLE department
(dept_id NUMBER(2),name VARCHAR2(15),budget NUMBER(11,2),projects ProjectList);
Setiap item di dalam kolom projects adalah varray yang akan menyimpan projects yang dijadwalkan untuk departemen tersebut.
Sekarang, kita telah siap untuk mempopulasi relational table department. Di dalam contoh berikut ini, perhatikan bagaimana constructor varray ProjectList() menyediakan nilai-nilai untuk kolom projects:
BEGIN
INSERT INTO department
VALUES(30,'Accounting', 1205700,ProjectList(Project(1,'Design New Expense Report', 3250),Project(2,'Outsource Payroll', 12350),Project(3,'Evaluate Merger Proposal', 2750),Project(4,'Audit Accounts Payable', 1425)));
INSERT INTO department
VALUES(50,'Maintenance', 925300,ProjectList(Project(1,'Repair Leak in Roof', 2850),Project(2,'Install New Door Locks', 1700),Project(3,'Wash FrontWindows', 975),Project(4,'Repair Faulty Wiring', 1350),Project(5,'Winterize Cooling System', 1125)));
INSERT INTO department
VALUES(60,'Security', 750400,ProjectList(Project(1,'Issue New Employee Badges', 13500),Project(2,'Find Missing IC Chips', 2750),Project(3,'Upgrade Alarm System', 3350),Project(4,'Inspect Emergency Exits', 1900)));
END;
Di dalam contoh berikut ini, kita meng-update daftar project yang diberikan untuk Security Department:
DECLARE
new_projects ProjectList := ProjectList(Project(1,'Issue New Employee Badges', 13500),Project(2,'Develop New Patrol Plan', 1250),Project(3,'Inspect Emergency Exits', 1900),Project(4,'Upgrade Alarm System', 3350),Project(5,'Analyze Local Crime Stats', 825));
BEGIN
UPDATE department
SET projects = new_projects
WHERE dept_id = 60;
END;
Di dalam contoh selanjutnya, kita menampilkan seluruh project untuk Accounting Department ke dalam varray lokal:
DECLARE
my_projects ProjectList;
BEGIN
SELECT projects
INTO my_projects
FROM department
WHERE dept_id = 30;
END;
Di dalam contoh terakhir, kita menghapus Accounting Department beserta daftar project-nya dari table department:
BEGIN
DELETE FROM department WHERE dept_id = 30;
END;
5.8.2. Memanipulasi Elemen Collection Secara Individual Menggunakan SQL
Secara default, operasi-operasi SQL menyimpan dan menampilkan seluruh collection dibandingkan elemen-elemen individual. Untuk memanipulasi elemen-elemen dari collection dengan menggunakan SQL, kita menggunakan operator TABLE. Operator TABLE menggunakan subquery untuk menggali varray atau nested table, sehingga perintah INSERT, UPDATE, atau DELETE diaplikasikan terhadap nested table dibandingkan top-level table.
Contoh: Menambahkan Elemen Kedalam Nested Table dengan SQL
Dalam contoh berikut ini, kita menambahkan baris data ke nested table History Department yang tersimpan di dalam kolom COURSES:
BEGIN
-- Operator TABLE membuat perintah diterapkan terhadap nested table
-- dari baris data 'History' dari table DEPARTMENT
INSERT INTO TABLE(SELECT courses FROM department WHERE name = 'History')
VALUES('Modern China');
END;
Contoh: Mengubah Elemen-elemen Dalam Nested Table dengan SQL
Dalam contoh berikutnya, kita menyingkat nama-nama untuk beberapa kursus yang ditawarkan oleh Phsycology Department:
BEGIN
UPDATE TABLE(SELECT courses FROM department WHERE name = 'Psychology')
SET credits = credits + adjustment
WHERE course_no IN (2200, 3540);
END;
Contoh: Menampilkan Elemen Tunggal dari Nested Table dengan SQL
Dalam contoh berikut ini, kita menampilkan judul dari kursus tertentu yang ditawarkan oleh History Department:
DECLARE
my_title VARCHAR2(64);
BEGIN
-- Kita mengetahui bahwa terdapat kursus sejarah dengan judul 'Etruscan'.
-- Query ini menampilkan judul lengkap dari nested table dari kursus
-- untuk History department.
SELECT title INTO my_title
FROM TABLE(SELECT courses FROM department WHERE name = 'History')
WHERE name LIKE '%Etruscan%';
END;
Contoh: Menghapus Elemen-elemen dari Nested Table dengan SQL
Dalam contoh selanjutnya, kita menghapus 5 kredit kursus yang ditawarkan oleh English Department:
BEGIN
DELETE TABLE(SELECT courses FROM department WHERE name = 'English')
WHERE credits = 5;
END;
Contoh: Menampilkan Elemen-elemen dari Varray dengan SQL
Dalam contoh di bawah ini, kita menampilkan judul dan biaya dari proyek keempat Maintenance Department dari varray kolom projects:
DECLARE
my_cost NUMBER(7,2);
my_title VARCHAR2(35);
BEGIN
SELECT cost, title INTO my_cost, my_title
FROM TABLE(SELECT projects FROM department WHERE dept_id = 50)
WHERE project_no = 4;
...
END;
Contoh: Melakukan Operasi-operasi INSERT, UPDATE, dan DELETE pada Varray dengan SQL
Saat ini, kita tidak dapat mereferensi elemen-elemen individual dari varray di dalam perintah INSERT, UPDATE, atau DELETE. Kita harus menampilkan seluruh varray, dan menggunakan perintah-perintah prosedural PL/SQL untuk menambahkan, menghapus, atau mengubah elemen-elemennya, dan kemudian menyimpan kembali varray yang telah kita ubah tersebut ke dalam database table.
CREATE PROCEDURE add_project (dept_no IN NUMBER, new_project IN Project, position IN NUMBER) AS
my_projects ProjectList;
BEGIN
SELECT projects INTO my_projects FROM department
WHERE dept_no = dept_id FOR UPDATE OF projects;
my_projects.EXTEND; -- menciptakan ruang untuk project baru
/* Memindah elemen-elemen varray ke posisi lebih depan. */
FOR i IN REVERSE position..my_projects.LAST - 1 LOOP
my_projects(i + 1) := my_projects(i);
END LOOP;
my_projects(position):= new_project; -- menambahkan project baru
UPDATE department
SET projects = my_projects
WHERE dept_no = dept_id;
END add_project;
Stored procedure di bawah ini mengubah project yang telah diberikan:
CREATE PROCEDURE update_project (dept_no IN NUMBER, proj_no IN NUMBER, new_title IN VARCHAR2 DEFAULT NULL,
new_cost IN NUMBER DEFAULT NULL) AS
my_projects ProjectList;
BEGIN
SELECT projects INTO my_projects FROM department
WHERE dept_no = dept_id FOR UPDATE OF projects;
/* Mencari project, mengubahnya, lalu segera keluar dari loop. */
FOR i IN my_projects.FIRST..my_projects.LAST LOOP
IF my_projects(i).project_no = proj_no THEN
IF new_title IS NOT NULL THEN
my_projects(i).title:= new_title;
END IF;
IF new_cost IS NOT NULL THEN
my_projects(i).cost:= new_cost;
END IF;
EXIT;
END IF;
END LOOP;
UPDATE department
SET projects = my_projects
WHERE dept_no = dept_id;
END update_project;
Contoh: Melakukan Operasi-operasi INSERT, UPDATE, dan DELETE pada Nested Table PL/SQL
Untuk melakukan operasi-operasi DML pada nested table PL/SQL, kita menggunakan operator-operator TABLE dan CAST. Dengan cara ini, kita dapat melakukan kumpulan operasi-operasi pada nested table menggunakan notasi SQL, tanpa benar-benar menyimpan nested table di dalam database.
Operand-operand CAST merupakan collection variable PL/SQL dan collection type SQL (diciptakan dengan perintah CREATE TYPE). CAST mengkonversi colleciton PL/SQL ke type SQL.
Contoh berikut menghitung jumlah perbedaan antara daftar kursus yang telah direvisi dan yang asli (perhatikan bahwa jumlah kredit untuk kursus 3720 berubah dari 4 ke 3).
DECLARE
revised CourseList := CourseList(Course(1002,'Expository Writing', 3),Course(2020,
'Film and Literature', 4),Course(2810,'Discursive Writing', 4),
Course(3010,'Modern English Grammar ', 3),Course(3550,
'Realism and Naturalism', 4),Course(3720,'Introduction to Shakespeare', 3),
Course(3760,'Modern Drama', 4),Course(3822,'The Short Story', 4),
Course(3870,'The American Novel', 5),Course(4210,'20th-Century Poetry', 4),
Course(4725,'Advanced Workshop in Poetry', 5));
num_changed INTEGER;
BEGIN
SELECT COUNT(*) INTO num_changed
FROM TABLE(CAST(revised AS CourseList)) new,TABLE(SELECT courses FROM department WHERE
name = 'English') AS old
WHERE new.course_no = old.course_no AND (new.title!= old.title OR new.credits != old.credits);
dbms_output.put_line(num_changed);
END;
5.9. Menggunakan Multilevel Collections
Sebagai tambahan untuk collections dari tipe-tipe scalar atau object, kita dapat pula menciptakan collections yang elemen-elemennya adalah collections. Sebagai contoh, kita dapat menciptakan nested table dari varray, varray dari varray, varray dari nested table, dan seterusnya.
Ketika menciptakan nested table dari nested table sebagai kolom di dalam SQL, periksa sintaks dari perintah CREATE TABLE untuk melihat bagaimana mendefinisikan peyimpanan table.
Berikut ini beberapa contoh yang menunjukkan sintaks dan kemungkinannya untuk multilevel collections.
Contoh Multilevel VARRAY
declare
type t1 is varray(10) of integer;
type nt1 is varray(10) of t1; -- varray multilevel
type va t1 := t1(2,3,5); -- menginisialisasi varray multilevel
nva nt1 := nt1(va, t1(55,6,73), t1(2,4), va);
i integer;
va1 t1;
begin
-- akses multilevel
i := nva(2)(3); -- saya akan mendapatkan nilai 73
dbms_output.put_line(i); -- menambahkan elemen ke nva
nva.extend;
nva(5):= t1(56, 32); -- mengganti elemen varray dalam
nva(4):= t1(45,43,67,43345); -- mengganti elemen integer dalam
nva(4)(4):= 1; -- mengganti 43345 dengan 1
-- menambahkan elemen baru ke elemen ke-4 varray
-- dan menyimpan integer 89 kepadanya.
nva(4).extend;
nva(4)(5):= 89;
end;
/
Contoh Multilevel Nested Table
declare
type tb1 is table of varchar2(20);
type ntb1 is table of tb1; -- table dari elemen-elemen table
type tv1 is varray(10) of integer;
type ntb2 is table of tv1; -- table dari elemen-elemen varray
vtb1 tb1 := tb1('one', 'three');
vntb1 ntb1 := ntb1(vtb1);
vntb2 ntb2 := ntb2(tv1(3,5), tv1(5,7,3)); -- table dari elemen-elemen varray
begin
vntb1.extend;
vntb1(2) := vntb1(1);
-- menghapus elemen pertama dalam vntb1
vntb1.delete(1);
-- menghapus string pertama dari table kedua dalam nested table
vntb1(2).delete(1);
end;
/
Contoh Multilevel Associative Varray
declare
type tb1 is table of integer index by binary_integer;
-- berikut ini adalah index-by table dari index-by tables
type ntb1 is table of tb1 index by binary_integer;
type va1 is varray(10) of varchar2(20);
-- berikut ini adalah index-by table dari varray elements
type ntb2 is table of va1 index by binary_integer;
v1 va1:= va1('hello', 'world');
v2 ntb1;
v3 ntb2;
v4 tb1;
v5 tb1; -- table kosong
begin
v4(1):= 34;
v4(2):= 46456;
v4(456):= 343;
v2(23):= v4;
v3(34):= va1(33, 456, 656, 343);
-- memberikan table kosong ke v2(35) dan coba lagi
v2(35):= v5;
v2(35)(2):= 78; -- dapat berfungsi sekarang
end;
/
Contoh Multilevel Collections dan Bulk SQL
create type t1 is varray(10) of integer;
/
create table tab1 (c1 t1);
insert into tab1 values (t1(2,3,5));
insert into tab1 values (t1(9345, 5634, 432453));
declare
type t2 is table of t1;
v2 t2;
begin
select c1 BULK COLLECT INTO v2 from tab1;
dbms_output.put_line(v2.count); -- menampilkan 2
end;
/
5.10. Menggunakan Method-method Collection
Method-method collection berikut ini membantu mengeneralisasi kode program, membuat collections lebih mudah untuk digunakan, dan membuat aplikasi-aplikasi kita lebih mudah untuk dipelihara:
EXISTS
COUNT
LIMIT
FIRST and LAST
PRIOR and NEXT
EXTEND
TRIM
DELETE
Method collection merupakan function dan procedure built-in (disediakan oleh Oracle) yang beroperasi terhadap collections dan dipanggil dengan menggunakan notasi titik. Sintaksnya adalah:
collection_name.method_name[(parameters)]
Method-method collection tidak dapat dipanggil dari perintah-perintah SQL. Juga, EXTEND dan TRIM tidak dapat digunakan bersama associative arrays. EXISTS, COUNT, LIMIT, FIRST, LAST, PRIOR, dan NEXT adalah function; EXTEND, TRIM, dan DELETE adalah procedure. EXISTS, PRIOR, NEXT, TRIM, EXTEND, dan DELETE mengambil parameter-parameter yang berhubungan dengan subscript-subscript collection, dimana biasanya integer namun dapat juga berupa string untuk associative arrays.
Hanya EXISTS yang dapat diterapkan secara atomik terhadap collections yang null. Juga kita menerapkan method lain terhadap collection tersebut, PL/SQL menampilkan COLLECTION_IS_NULL.
5.10.1. Memeriksa Apakah Elemen Collection Ada (Method EXISTS)
EXISTS(n) menghasilkan TRUE jika elemen ke n di dalam collection ada. Sebaliknya, EXISTS(n) menghasilkan FALSE. Sebagian besar, kita menggunakan EXISTS dengan DELETE untuk memelihara nested tables yang tipis atau jarang. Kita dapat juga menggunakan EXISTS untuk mengabaikan munculnya exception ketika kita mereferensi elemen yang tidak ada. Dalam contoh berikut ini, PL/SQL mengeksekusi perintah pemberian nilai (assignment statement) hanya jika elemen i ada:
IF courses.EXISTS(i) THEN courses(i) := new_course; END IF;
Ketika melewatkan subscript yang out-of-range, EXIST menghasilkan FALSE dibanding memunculkan exception SUBSCRIPT_OUTSIDE_LIMIT.
5.10.2. Menghitung Elemen di dalam Collection (Method COUNT)
COUNT menghasilkan jumlah elemen yang terkandung dalam collection. Sebagai contoh, jika varray projects mengandung 25 elemen, kondisi IF berikut ini bernilai TRUE:
IF projects.COUNT = 25 THEN ...
COUNT berguna karena ukuran terkini dari collection tidak selalu dapat diketahui. Sebagai contoh, jika kita meletakkan (fetch) kolom dari Oracle data ke dalam nested table, berapa banyak elemen yang dikandung oleh table tersebut? COUNT memberikan kita jawabannya.
Kita dapat menggunakan COUNT dimanapun ekspresi integer diperbolehkan. Dalam contoh selanjutnya, kita menggunakan COUNT untuk menentukan batas atas dari jangkauan perulangan:
FOR i IN 1..courses.COUNT LOOP ...
Untuk varrays, COUNT selalu sama dengan LAST. Untuk nested tables, COUNT secara normal sama dengan LAST. Namun, jika kita menghapus elemen-elemen dari tengah nested table, COUNT akan menjadi lebih kecil daripada LAST.
Ketika menghitung elemen, COUNT mengabaikan elemen-elemen yang telah terhapus.
5.10.3. Memeriksa Ukuran Maksimum dari Collection (Method LIMIT)
Untuk nested tables dan associative arrays, dimana kita tidak memiliki ukuran maksimum, LIMIT menghasilkan NULL. Untuk varrays, LIMIT menghasilkan jumlah maksimum elemen yang dapat dimiliki oleh varray (dimana kita harus menentukannya di dalam definisi type, dan dapat mengubahnya kemudian dengan method TRIM dan EXTEND). Sebagai contoh, jika ukuran maksimum dari varray PROJECTS adalah 25 elemen, kondisi IF berikut ini TRUE:
IF projects.LIMIT = 25 THEN ...
Kita dapat menggunakan LIMIT dimanapun ekspresi integer diperbolehkan. Dalam contoh berikut ini, kita menggunakan LIST untuk menentukan apakah kita dapat menambahkan 15 elemen ke varray projects:
IF (projects.COUNT + 15) < projects.LIMIT THEN ...
5.10.4.Mencari Elemen Collection Pertama atau Terakhir (Method FIRST dan LAST)
FIRST dan LAST menghasilkan nomor index pertama dan terakhir (terkecil dan terbesar) di dalam collection. Untuk associative array dengan nilai kunci VARCHAR2, nilai kunci terendah dan tertinggi dihasilkan; urutannya berdasarkan pada nilai-nilai biner dari karakter di dalam string, kecuali parameter NLS_COMP di atur ke ANSI, dimana pengurutan didasarkan pada pengurutan locale-specific yang ditentukan oleh parameter NLS_SORT.
Jika collection kosong, FIRST dan LAST menghasilkan NULL.
Jika collection hanya terdiri dari satu elemen, FIRST dan LAST menghasilkan nilai index yang sama:
IF courses.FIRST = courses.LAST THEN ... -- hanya satu elemen
Contoh berikutnya menunjukkan bahwa kita dapat menggunakan FIRST dan LAST untuk menentukan bahwa batas bawah dan atas dari jangkauan yang disediakan bagi setiap elemen di dalam jangkauan tersebut ada:
FOR i IN courses.FIRST..courses.LAST LOOP ...
Pada kenyataannya, kita dapat menggunakan FIRST atau LAST dimanapun ekspresi integer diperbolehkan. Dalam contoh berikut ini, kita menggunakan FIRST untuk menginisialisasi loop counter:
i := courses.FIRST;
WHILE i IS NOT NULL LOOP ...
Untuk varrays, FIRST selalu menghasilkan 1 dan LAST selalu sama dengan COUNT. Untuk nested tables, FIRST normalnya menghasilkan 1. Namun, jika kita menghapus elemen-elemen dari awal nested table, FIRST menghasilkan angka lebih besar daripada 1. Juga untuk nested tables, LAST normalnya sama dengan COUNT. Namun, jika kita menghapus elemen-elemen dari tengah nested table, LAST menjadi lebih besar daripada COUNT.
Ketika membaca elemen-elemen, FIRST dan LAST mengabaikan elemen-elemen yang telah terhapus.
5.10.5. Perulangan Melewati Elemen-elemen Collection (Method PRIOR dan NEXT)
PRIOR(n) menghasilkan nomor index yang mendahului index n di dalam collection. NEXT(n) menghasilkan nomor index yang mengikuti index n. Jika n tidak memiliki pendahulu, PRIOR(n) menghasilkan NULL. Demikian juga, jika n tidak memiliki pengikut, NEXT(n) menghasilkan NULL.
Untuk associative arrays dengan kunci VARCHAR2, method-method ini menghasilkan nilai kunci yang sesuai; urutannya didasarkan pada nilai-nilai biner dari karakter-karakter di dalam string, kecuali parameter NLS_COMP di set ke ANSI, di mana dalam kasus ini urutan didasarkan pada order pengurutan local-specific yang ditentukan oleh parameter NLS_SORT.
Method-method ini lebih dapat diandalkan daripada perulangan melalui kumpulan tetap dari nilai-nilai subscript, karena elemen-elemen dapat ditambahkan dan dihapus dari collection selama perulangan. Hal ini benar khususnya untuk associative arrays, dimana subscript-subscript tidak boleh berada di dalam order yang bertalian dan sehingga rangkaian subscript-subscript mungkin (1,2,4,8,16) atau (‘A’,'E’,'I’,'O’,'U’).
PRIOR dan NEXT tidak mengikat dari satu akhir dari collection kepada lainnya. Sebagai contoh, perintah berikut ini memberikan NULL ke n karena elemen pertama di dalam collection tidak memiliki pendahulu:
n := courses.PRIOR(courses.FIRST); -- memberikan NULL kepada n
PRIOR adalah kebalikan dari NEXT untuk melintasi collection-collection yang diindex oleh suatu rangkaian subscript-subscript. Di dalam contoh berikut ini, kita menggunakan NEXT untuk melintasi nested table dimana beberapa elemen-elemen telah dihapus:
i := courses.FIRST; -- mendapatkan subscript dari elemen pertama
WHILE i IS NOT NULL LOOP -- melakukan sesuatu dengan courses(i)
i := courses.NEXT(i); -- mendapatkan subscript dari elemen berikutnya
END LOOP;
Ketika melintasi elemen-elemen, PRIOR dan NEXT mengabaikan elemen-elemen yang telah terhapus.
5.10.6. Memperbesar Ukuran Collection (Method EXTEND)
Untuk memperbesar ukuran nested table atau varray, kita menggunakan EXTEND. Kita tidak dapat menggunakan EXTEND dengan index-by tables.
Procedure ini memiliki tiga bentuk:
* EXTEND menambahkan sebuah elemen null kepada collection.
* EXTEND(n) menambahkan n elemen null kepada collection.
* EXTEND(n,i) menambahkan n kopi dari elemen ke i kepada collection.
Sebagai contoh, perintah berikut ini menambahkan 5 kopi dari elemen 1 terhadap nested table courses:
courses.EXTEND(5,1);
Kita tidak dapat menggunakan EXTEND untuk menginisialisasi collection null secara atomik. Juga, jika kita memaksakan constraint NOT NULL pada tipe TABLE atau VARRAY, kita tidak dapat menerapkan dua bentuk pertama dari EXTEND untuk collection-collection dari tipe tersebut.
EXTEND beroperasi pada ukuran internal dari collection, yang termasuk beberapa elemen-elemen yang telah dihapus. Jadi, jika EXTEND menemui elemen-elemen yang telah dihapus, ia mengikutsertakannya di dalam hitungannya. PL/SQL memelihara tempat untuk elemen-elemen yang telah dihapus sehingga kita dapat menggantinya jika dikehendaki. Marilah kita perhatikan contoh berikut ini:
DECLARE
TYPE CourseList IS TABLE OF VARCHAR2(10);
courses CourseList;
BEGIN
courses:= CourseList('Biol 4412', 'Psyc 3112', 'Anth 3001');
courses.DELETE(3);-- menghapus elemen 3
/* PL/SQL menyediakan tempat untuk elemen 3. Sehingga, perintah berikutnya menambahkan elemen 4, bukan elemen 3. */
courses.EXTEND; -- menambahkan satu elemen null
/* Sekarang elemen 4 telah ada, sehingga perintah berikutnya tidak menyebabkan SUBSCRIPT_BEYOND_COUNT. */
courses(4):= 'Engl 2005';
Ketika ia mengandung elemen-elemen yang telah terhapus, ukuran internal dari nested table berbeda dengan nilai-nilai yang dihasilkan oleh COUNT dan LAST. Sebagai contoh, jika kita menginisialisasi nested table dengan lima elemen, lalu menghapus elemen 2 dan 5, ukuran internalnya adalah 5, COUNT menghasilkan 3, dan LAST menghasilkan 4. Seluruh elemen-elemen yang telah terhapus (baik di depan, tengah, atau belakang) diperlakukan sama.
5.10.7. Memperkecil Ukuran Collection (Method TRIM)
Procedure ini memiliki dua bentuk:
* TRIM menghapus elemen dari posisi akhir collection
* TRIM(n) menghapus n elemen dari posisi akhir collection
Sebagai contoh, perintah ini menghapus tiga elemen terakhir dari nested table courses:
courses.TRIM(3);
Jika n terlalu besar, TRIM(n) memunculkan SUBSCRIPT_BEYOND_COUNT.TRIM beroperasi pada ukuran internal collection. Jadi, jika TRIM berjumpa dengan elemen yang telah dihapus, ia menyertakannya dalam rangkaiannya. Perhatikan contoh berikut ini:
DECLARE
TYPE CourseList IS TABLE OF VARCHAR2(10);
courses CourseList;
BEGIN
courses := CourseList('Biol 4412', 'Psyc 3112', 'Anth 3001');
courses.DELETE(courses.LAST); -- menghapus elemen 3
/* Pada titik ini, COUNT sama dengan 2, jumlah elemen-elemen tetap. Sehingga, kita
mungkin berharap perintah berikutnya untuk nested table kosong dengan memotong elemen 1 dan 2.
Daripada itu, ia memotong elemen valid 2 dan menghapus elemen 3 karena TRIM menyertakan elemen-elemen
yang telah terhapus ke dalam hitungannya.
courses.TRIM(courses.COUNT);
dbms_output.put_line(courses(1)); -- menampilkan 'Biol 4412'
Secara umum, tidak bergantung kepada interaksi antara TRIM dan DELETE. Adalah lebih baik untuk menganggap nested table seperti array berukuran tetap (fixed-size arrays) dan hanya menggunakan DELETE, atau menganggap mereka seperti stacks serta hanya menggunakan TRIM dan EXTEND.
PL/SQL tidak menyediakan tempat untuk elemen-elemen yang telah dipotong. Sehingga, kita tidak dapat mengganti elemen-elemen tersebut secara sederhana dengan memberikan nilai baru terhadapnya.
5.10.8. Menghapus Elemen-elemen Collection (Method DELETE)
Procedure ini memiliki berbagai bentuk:
* DELETE menghapus seluruh elemen dari collection
* DELETE (n) menghapus elemen ke-n dari associative array dengan kunci numeric atau dari nested table. Jika associative array memiliki kunci string, elemen yang berhubungan dengan nilai kunci tersebut ikut dihapus. Jika n adalah null, DELETE(n) tidak melakukan apapun
* DELETE(m,n) menghapus seluruh elemen di dalam jangkauan m..n dari associative array atau nested table. Jika m lebih besar dari n atau jika m atau n adaah null, DELETE(m,n) tidak melakukan apapun.
Sebagai contoh:
BEGIN
courses.DELETE(2); -- mengapus elemen 2
courses.DELETE(7,7); -- mengapus elemen 7
courses.DELETE(6,3); -- tidak melakukan apapun
courses.DELETE(3,6); -- mengapus elemen 3 sampai 6
projects.DELETE; -- mengapus seluruh elemen
nicknames.DELETE('Chip'); -- mengapus elemen yang ditunjuk oleh kunci ini
nicknames.DELETE('Buffy','Fluffy'); -- menghapus elemen-elemen dengan kunci
-- dalam jangkauan alphabetic ini
END;
Varray adalah padat, sehingga kita tidak dapat menghapus individual elemennya.
Jika elemen yang akan dihapus tidak ada, DELETE akan dilewati; tidak ada exception yang muncul. PL/SQL menyediakan tempat untuk elemen-elemen yang telah dihapus. Sehingga, kita dapat mengganti elemen tersebut secara sederhana dengan memberikannya nilai baru.
DELETE mengijinkan kita untuk memelihara nested table yang jarang. Dalam contoh berikut ini, kita menampilkan nested table prospects ke dalam table sementara, memangkasnya, lalu menyimpannya kembali ke dalam database:
DECLARE
my_prospects ProspectList;
revenue NUMBER;
BEGIN
SELECT prospects
INTO my_prospects
FROM customers WHERE ...
FOR i IN my_prospects.FIRST..my_prospects.LAST LOOP
estimate_revenue(my_prospects(i),revenue); -- memanggil procedure
IF revenue < 25000 THEN
my_prospects.DELETE(i);
END IF;
END LOOP;
UPDATE customers
SET prospects = my_prospects
WHERE ...
Jumlah memory yang dialokasikan untuk nested table dapat bertambah dan berkurang secara dinamis. Begitu kita menghapus elemen-elemen, memory dibebaskan halaman demi halaman. Jika kita menghapus seluruh table, seluruh memory dibebaskan.
5.10.9. Menerapkan Method-method untuk Parameter-parameter Collection
Bersama sebuah subprogram, parameter dari collection mengasumsikan properties dari argumen yang membatasi kepadanya. Sehingga, kita dapat menerapkan method-method collection built-in (FIRST, LAST, COUNT, dan seterusnya) untuk parameter-parameter seperti itu. Dalam contoh berikut ini, nested table dideklarasikan sebagai parameter formal dari procedure ter-package:
CREATE PACKAGE personnel AS TYPE Staff IS TABLE OF Employee;
...
PROCEDURE award_bonuses(members IN Staff);
END personnel;
CREATE PACKAGE BODY personnel AS
...
PROCEDURE award_bonuses(members IN Staff) IS
BEGIN
...
IF members.COUNT > 10 THEN -- menerapkan method
...
END IF;
END;
END personnel;
Catatan: Untuk paramter-parameter varray, nilai LIMIT selalu diturunkan dari definisi tipe parameter, tanpa memperhatikan mode parameter.
5.11. Menghindari Exception Collection
Dalam banyak kasus, jika kita mereferensi elemen collection yang tidak ada, PL/SL memunculkan predefined exception. Mari kita perhatikan contoh berikut ini:
DECLARE
TYPE NumList IS TABLE OF NUMBER;
nums NumList; -- secara atomik null
BEGIN
/* Assume execution continues despite the raised exceptions. */
nums(1) := 1; -- menyebabkan COLLECTION_IS_NULL (1)
nums := NumList(1,2); -- menginisialisasi table
nums(NULL) := 3 -- menyebabkan VALUE_ERROR (2)
nums(0) := 3; -- menyebabkan SUBSCRIPT_OUTSIDE_LIMIT (3)
nums(3) := 3; -- menyebabkan SUBSCRIPT_BEYOND_COUNT (4)
nums.DELETE(1); -- menghapus elemen 1
IF nums(1) = 1 THEN ... -- menyebabkan NO_DATA_FOUND (5)
Di dalam kasus pertama, nested table secara atomik adalah null. Dalam kasus kedua, subscript adalah null. Dalam kasus ketiga, subscript berada di luar jangkauan yang diperbolehkan. Dalam kasus keempat, subscript melampaui jumlah elemen dalam table. Dalam kasus kelima, subscript menunjuk kepada elemen yang telah terhapus.
Daftar berikut ini menunjukkan kapan exception tertentu muncul:
* COLLECTION_IS_NULL kita berusaha beroperasi pada collection yang secara atomik null.
* NO_DATA_FOUND a subscript designates an element that was deleted, or a nonexistent element of an associative array.
* SUBSCRIPT_BEYOND_COUNT a subscript exceeds the number of elements in a collection.
* SUBSCRIPT_OUTSIDE_LIMIT a subscript is outside the allowed range.
* VALUE_ERROR a subscript is null or not convertible to the key type. This exception might occur if the key is defined as a
* PLS_INTEGER range, and the subscript is outside this range.
Dalam beberap kasus, kita dapat melewatkan subscript-subscript yang tidak benar (invalid) kepada method tanpa menyebabkan munculnya exception. Sebagai contoh, ketika kita melewatkan subscript null kepada procedure DELETE, ia tidak melakukan apapun. Juga, kita dapat mengganti elemen-elemen yang telah terhapus tanpa memunculkan NO_DATA_FOUND, seperti ditunjukkan oleh contoh di bawah ini:
DECLARE
TYPE NumList IS TABLE OF NUMBER;
nums NumList := NumList(10,20,30); -- menginisialisasi table
BEGIN
nums.DELETE(-1); -- tidak menyebabkan SUBSCRIPT_OUTSIDE_LIMIT
nums.DELETE(3); -- menghapus elemen ke-3
dbms_output.put_line(nums.COUNT); -- menampilkan 2
nums(3) := 30; -- diperbolehkan; tidak menyebabkan NO_DATA_FOUND
dbms_output.put_line(nums.COUNT); -- menampilkan 3
END;
Tipe-tipe collection ter-package dan tipe-tipe collection lokal tidak pernah kompatibel. Sebagai contoh, bayangkan kita ingin memanggil procedure ter-package berikut ini:
CREATE PACKAGE pkg1 AS
TYPE NumList IS VARRAY(25) OF NUMBER(4);
PROCEDURE delete_emps(emp_list NumList);
END pkg1;
CREATE PACKAGE BODY pkg1 AS
PROCEDURE delete_emps(emp_list NumList) IS ...
...
END pkg1;
Ketika kita menjalankan blok PL/SQL di bawah ini, pemanggilan procedure kedua gagal dengan pesan kesalahan wrong number atau types of arguments. Hal ini dikarenakan package dan tipe-tipe VARRAY lokal tidak kompatibel meskipun definisi-definisi mereka identik.
DECLARE
TYPE NumList IS VARRAY(25) OF NUMBER(4);
emps pkg1.NumList := pkg1.NumList(7369, 7499);
emps2 NumList := NumList(7521, 7566);
BEGIN
pkg1.delete_emps(emps);
pkg1.delete_emps(emps2); -- menyebabkan kesalahan kompilasi
END;
5.12. Mengurangi Ongkos Perulangan untuk Collections dengan Bulk Binds
Seperti ditunjukkan oleh Gambar 5-3, PL/SQL engine mengeksekusi perintah-perintah prosedural namun mengirimkan perintah-perintah SQL kepada SQL engine, yang mengeksekusi perintah-perintah SQL dan, dalam beberapa kasus, menghasilkan data untuk dikirimkan ke PL/SQL engine.

Gambar 5-3 Perpindahan Konteks
Terlalu banyak perpindahan konteks di antara PL/SQL dan SQL engine dapat membahayakan performa. Hal ini dapat terjadi ketika loop mengeksekusi perintah SQL yang terpisah untuk setiap elemen dari collection, menentukan elemen collection sebagai bind variable. Sebagai contoh, perintah DELETE berikut ini dikirimkan ke SQL engine dengan setiap iterasi dari perulangan FOR:
DECLARE
TYPE NumList IS VARRAY(20) OF NUMBER;
depts NumList := NumList(10, 30, 70); -- department numbers
BEGIN
...
FOR i IN depts.FIRST..depts.LAST LOOP
DELETE FROM emp
WHERE deptno = depts(i);
END LOOP;
END;
Dalam kasus seperti ini, jika perintah SQL mempengaruhi empat atau lebih baris-baris data pada database, penggunaan bulk binds dapat dipertimbangkan untuk meningkatkan performa.
5.12.1. Bagaimana Bulk Binds Meningkatkan Performa?
Pemberian nilai kepada variable-variable PL/SQL di dalam perintah-perintah SQL disebut binding. Operasi-operasi binding PL/SQL terbagi menjadi tiga kategori:
* in-bind Ketika variable PL/SQL atau variable host disimpan di dalam database dengan perintah INSERT atau UPDATE.
* out-bind Ketika nilai database diberikan ke variable PL/SQL atau variable host dengan klausaRETURNING dari perintah-perintah INSERT, UPDATE, atau DELETE.
* define Ketika nilai database diberikan ke variable PL/SQL atau variable host dengan perintah SELECT atau FETCH.
Perintah DML dapat mentransfer seluruh elemen-elemen dari collection dalam sebuah operasi tunggal, dan proses ini disebut dengan bulk binding. Jika collection memiliki 20 elemen, bulk binding mengijinkan kita untuk melakukan sesuatu yang sama dengan 20 perintah-perintah SELECT, INSERT, UPDATE, atau DELETE dengan menggunakan operasi tunggal. Teknik ini meningkatkan performa dengan meminimalkan jumlah perpindahan konteks di antara PL/SQL engine dan SQL engine. Dengan bulk binds, seluruh collection, tidak hanya elemen-elemen individual, dilewatkan kembali dan seterusnya.
Untuk melakukan bulk binds dengan perintah-perintah INSERT, UPDATE, dan DELETE, kita mengapit perintah SQL di dalam perintah FORALL PL/SQL.
Untuk melakukan bulk binds dengan perintah-perintah SELECT, kita menyertakan klausa BULK COLLECT di dalam perintah SELECT dibandingkan menggunakan INTO.
Contoh: Melakukan Bulk Bind dengan DELETE
Perintah DELETE berikut ini dikirimkan ke SQL engine hanya sekali, walaupun ia melakukan tiga operasi DELETE:
DECLARE
TYPE NumList IS VARRAY(20) OF NUMBER;
depts NumList := NumList(10, 30, 70); -- department numbers
BEGIN
FORALL i IN depts.FIRST..depts.LAST
DELETE FROM emp
WHERE deptno = depts(i);
END;
Contoh: Melakukan Bulk Bind dengan INSERT
Dalam contoh di bawah ini, 5000 part number dan nama part di-load ke index-by tables. Seluruh elemen table ditambahkan ke database table sebanyak dua kali: pertama menggunakan perulangan FOR, lalu menggunakan perintah FORALL. Versi FORALL lebih cepat.
SQL> SET SERVEROUTPUT ON
SQL> CREATE TABLE parts(pnum NUMBER(4), pname CHAR(15));
Table created.
SQL> GET test.sql
1 DECLARE
2 TYPE NumTab IS TABLE OF NUMBER(4) INDEX BY BINARY_INTEGER;
3 TYPE NameTab IS TABLE OF CHAR(15) INDEX BY BINARY_INTEGER;
4 pnums NumTab;
5 pnames NameTab;
6 t1 NUMBER(5);
7 t2 NUMBER(5);
8 t3 NUMBER(5);
9
10
11 BEGIN
12 FOR j IN 1..5000 LOOP -- load index-by tables
13 pnums(j) := j;
14 pnames(j) := 'Part No. '|| TO_CHAR(j);
15 END LOOP;
16 t1 := dbms_utility.get_time;
17 FOR i IN 1..5000 LOOP -- menggunakan perulangan FOR
18 INSERT INTO parts VALUES(pnums(i), pnames(i));
19 END LOOP;
20 t2 := dbms_utility.get_time;
21 FORALL i IN 1..5000 -- menggunakan perintah FORALL
22 INSERT INTO parts VALUES(pnums(i), pnames(i));
23 get_time(t3);
24 dbms_output.put_line('Execution Time (secs)');
25 dbms_output.put_line('---------------------');
26 dbms_output.put_line('FOR loop: ' || TO_CHAR(t2 - t1));
27 dbms_output.put_line('FORALL: ' || TO_CHAR(t3 - t2));
28* END;
SQL> /
Execution Time (secs)
---------------------
FOR loop: 32
FORALL: 3
PL/SQL procedure successfully completed.
5.13. Menggunakan Perintah FORALL
Kata kunci FORALL menginstruksikan PL/SQL engine untuk melakukan bulk-bind terhadap kumpulan input sebelum mengirimkannya ke SQL engine. Meskipun perintah FORALL mengandung pola iterasi, ia bukan perulangan FOR. Sintaksnya adalah:
FORALL index IN lower_bound..upper_bound
sql_statement;
Index dapat direferensi hanya bersama perintah FORALL dan hanya sebagai subscript collection. Perinyah SQL harus perintah INSERT, UPDATE, atau DELETE yang mereferensi elemen-elemen collection. Dan, batas-batasnya harus menentukan jangkauan yang valid dari bilangan-bilangan index yang bertalian. SQL engine mengeksekusi perintah SQL sekali untuk setiap bilangan index di dalam jangkauan.
Contoh: Menggunakan FORALL dengan Bagian dari Collection
Seperti ditunjukkan oleh contoh berikut ini, batasan-batasan dari perulangan FORALL dapat diterapkan kepada bagian dari collection, tidak perlu kepada seluruh elemen:
DECLARE
TYPE NumList IS VARRAY(10) OF NUMBER;
depts NumList := NumList(20,30,50,55,57,60,70,75,90,92);
BEGIN
FORALL j IN 4..7 -- bulk-bind hanya bagian dari varray
UPDATE emp
SET sal = sal * 1.10
WHERE deptno = depts(j);
END;
Contoh: Bulk Bind Memerlukan Collection Ter-subscript
Perintah SQL dapat mereferensi lebih dari satu collection. Bagaimanapun, PL/SQL engine melakukan bulk-binds hanya terhadap collection ter-subscript. Sehingga, dalam contoh berikut ini, ia tidak melakukan bulk-bind terhadap collection sals, yang mana dilewatkan ke function median:
FORALL i IN 1..20
INSERT INTO emp2
VALUES (enums(i), names(i), median(sals), ...);
Contoh: Menambahkan Data ke Object Table dengan FORALL
Sebagai tambahan untuk table-table relasional, perintah FORALL dapat memanipulasi objek tables, seperti ditunjukkan oleh contoh berikut ini:
CREATE TYPE PNum AS OBJECT (n NUMBER);
/
CREATE TABLE partno OF PNum;
DECLARE
TYPE NumTab IS TABLE OF NUMBER;
nums NumTab := NumTab(1, 2, 3, 4);
TYPE PNumTab IS TABLE OF PNum;
pnums PNumTab := PNumTab(PNum(1), PNum(2), PNum(3), PNum(4));
BEGIN
FORALL i IN pnums.FIRST..pnums.LAST
INSERT INTO partno VALUES(pnums(i));
FORALL i IN nums.FIRST..nums.LAST
DELETE FROM partno
WHERE n = 2 * nums(i);
FORALL i IN nums.FIRST..nums.LAST
INSERT INTO partno
VALUES(100 + nums(i));
END;
5.13.1. Bagaimana FORALL Mempengaruhi Rollbacks
Dalam perintah FORALL, jika suatu eksekusi dari perintah SQL menyebabkan munculnya unhandled exception, seluruh perubahan database yang dibuat selama sebelum eksekusi akan dibatalkan. Namun, jika exception yang muncul tertangkap dan dapat ditangani, perubahan akan dibatalkan ke titik savepoint implisit yang ditandai sebelum eksekusi perintah SQL. Perubahan yang dibuat selama eksekusi-eksekusi sebelumnya tidak akan dibatalkan. Sebagai contoh, andaikata kita menciptakan database table yang menyimpan nomor departemen dan jabatan, seperti contoh berikut ini:
CREATE TABLE emp2 (deptno NUMBER(2), job VARCHAR2(15));
Berikutnya, kita menambahkan beberapa baris data ke dalam table, seperti berikut:
INSERT INTO emp2 VALUES(10, 'Clerk');
INSERT INTO emp2 VALUES(10, 'Clerk');
INSERT INTO emp2 VALUES(20, 'Bookkeeper'); -- 10-karakter jabatan
INSERT INTO emp2 VALUES(30, 'Analyst');
INSERT INTO emp2 VALUES(30, 'Analyst');
Kemudian, kita berusaha menambahkan 7 karakter string ‘ (temp)’ kepada jabatan tertentu menggunakan perintah UPDATE berikut ini:
DECLARE
TYPE NumList IS TABLE OF NUMBER;
depts NumList := NumList(10, 20, 30);
BEGIN
FORALL j IN depts.FIRST..depts.LAST
UPDATE emp2 SET job = job || ' (temp)'
WHERE deptno = depts(j); -- memunculkan exception "value too large"
EXCEPTION
WHEN OTHERS THEN
COMMIT;
END;
SQL engine mengeksekusi perintah UPDATE tiga kali, sekali untuk setiap bilangan index di dalam jangkauan tertentu, yaitu, sekali untuk depts(10), sekali untuk depts(20), dan sekali untuk depts(30). Eksekusi pertama berhasil, namun eksekusi kedua gagal karena nilai string ‘Bookkeeper (temp)’ terlalu besar untuk kolom job. Dalam kasus ini, hanya eksekusi kedua yang dibatalkan (rolled back).
Ketika suatu eksekusi dari perintah SQL menyebabkan munculnya exception, perintah FORALL berhenti. Dalam contoh kita, eksekusi kedua dari perintah UPDATE menyebabkan munculnya exception, sehingga eksekusi ketiga tidak pernah selesai.
5.13.2. Menghitung Baris Data yang Terpengaruh oleh Iterasi FORALL dengan Attribute %BULK_ROWCOUNT
Untuk memproses perintah-perintah manipulasi data SQL, SQL engine membuka cursor implisit bernama SQL. Attribute scalar dari cursor ini, %FOUND, %ISOPEN, %NOTFOUND, dan %ROWCOUNT, menghasilkan informasi yang berguna mengenai perintah manipulasi data SQL yang paling sering dieksekusi.
Cursor SQL memiliki satu attribute komposit, %BULK_ROWCOUNT, yang didesain untuk penggunaan bersama perintah FORALL. Attribute ini memiliki semantik dari index-by table. Elemen ke i -nya menyimpan jumlah baris data yang diproses oleh eksekusi ke i dari perintah INSERT, UPDATE, atau DELETE. Jika eksekusi ke i tidak mempengaruhi baris data, %BULK_ROWCOUNT(i) menghasilkan nol. Contohnya adalah sebagai berikut:
DECLARE
TYPE NumList IS TABLE OF NUMBER;
depts NumList := NumList(10, 20, 50);
BEGIN
FORALL j IN depts.FIRST..depts.LAST
UPDATE emp
SET sal = sal * 1.10
WHERE deptno = depts(j);
-- Apakah perintah UPDATE ke-3 berpengaruh terhadap suatu baris data?
IF SQL%BULK_ROWCOUNT(3) = 0 THEN ...
END;
Perintah FORALL dan attribute %BULK_ROWCOUNT menggunakan subscript-subscript yang sama. Sebagai contoh, jika FORALL menggunakan jangkauan 0..5, begitu juga dengan %BULK_ROWCOUNT.
%BULK_ROWCOUNT biasanya sama dengan 1 untuk inserts, karena sifat operasi insert berpengaruh hanya terhadap baris data tunggal. Namun untuk bentuk INSERT … SELECT, %BULK_ROWCOUNT dapat lebih besar daripada 1. Sebagai contoh, perintah FORALL di bawah ini menambahkan jumlah baris data yang berbeda-beda pada setiap iterasi. Setelah setiap iterasi, %BULK_ROWCOUNT menghasilkan jumlah item-item yang dimasukkan:
SET SERVEROUTPUT ON;
DECLARE
TYPE num_tab IS TABLE OF NUMBER;
deptnums num_tab;
BEGIN
SELECT deptno BULK COLLECT
INTO deptnums
FROM DEPT;
FORALL i IN 1..deptnums.COUNT
INSERT INTO emp_by_dept
SELECT empno, deptno
FROM emp WHERE deptno = deptnums(i);
FOR i IN 1..deptnums.COUNT LOOP
-- Menghitung berapa banyak baris data yang telah ditambahkan
-- untuk setiap departemen; dan dengan demikian, berapa banyak
-- karyawan di dalam setiap departemen.
dbms_output.put_line('Dept'||deptnums(i)||': inserted '||
SQL%BULK_ROWCOUNT(i)||'records');
END LOOP;
dbms_output.put_line('Total records inserted =' || SQL%ROWCOUNT);
END;
/
Kita dapat juga menggunakan attribute scalar %FOUND, %NOTFOUND, dan %ROWCOUNT dengan bulk binds. Sebagai contoh, %ROWCOUNT menghasilkan jumlah total baris data yang diproses oleh seluruh eksekusi perintah SQL.
%FOUND dan %NOTFOUND mengacu hanya kepada eksekusi terakhir dari perintah SQL. Namun, kita dapat menggunakan %BULK_ROWCOUNT untuk menyimpulkan nilai-nilai mereka untuk eksekusi-eksekusi individual. Sebagai contoh, ketika%BULK_ROWCOUNT(i) adalah nol, %FOUND dan %NOTFOUND adalah FALSE dan TRUE, secara berturut-turut.
5.13.3. Menangani Exceptions FORALL dengan Attribute %BULK_EXCEPTIONS
PL/SQL menyediakan mekanisme untuk menangani exceptions yang muncul selama eksekusi perintah FORALL. Mekanisme ini memungkinkan operasi bulk-bind untuk menyimpan informasi tentang exceptions dan melanjutkan pemrosesan.
Untuk membuat bulk bind diselesaikan meskipun terjadi errors, kita menambahkan kata kunci SAVE EXCEPTIONS terhadap perintah FORALL kita. Sintaksnya adalah sebagai berikut:
FORALL index IN lower_bound..upper_bound SAVE EXCEPTIONS
{insert_stmt | update_stmt | delete_stmt}
Seluruh exceptions yang muncul selama eksekusi disimpan di dalam attribute cursor baru %BULK_EXCEPTIONS, yang menyimpan kumpulan records. Setiap record memiliki dua field. Field pertama, %BULK_EXCEPTIONS(i).ERROR_INDEX, memegang “iterasi” dari perintah FORALL selama dimana exception muncul. Field kedua, %BULK_EXCEPTIONS(i).ERROR_CODE, memegang error code Oracle yang terkait.
Nilai-nilai yang disimpan oleh %BULK_EXCEPTIONS selalu mengacu kepada perintah FORALL yang paling sering dieksekusi. Jumlah exceptions disimpan di dalam attribute penghitung %BULK_EXCEPTIONS, yaitu %BULK_EXCEPTIONS.COUNT. Subscript-subscriptnya berjangkauan dari 1 hingga COUNT.
Jika kita mengabaikan kata kunci SAVE EXCEPTIONS, eksekusi dari perintah FORALL berhenti ketika exception muncul. Dalam kasus tersebut, SQL%BULK_EXCEPTIONS.COUNT menghasilkan 1, dan SQL%BULK_EXCEPTIONS berisi hanya satu record. Jika tidak terdapat exception yang muncul selama eksekusi, SQL%BULK_EXCEPTIONS.COUNT menghasilkan 0.
Contoh berikut ini menunjukkan bagaimana bergunanya attribute cursor %BULK_EXCEPTIONS dapat berupa:
DECLARE
TYPE NumList IS TABLE OF NUMBER;
num_tab NumList := NumList(10,0,11,12,30,0,20,199,2,0,9,1);
errors NUMBER;
dml_errors EXCEPTION;
PRAGMA exception_init(dml_errors, -24381);
BEGIN
FORALL i IN num_tab.FIRST..num_tab.LAST SAVE EXCEPTIONS
DELETE FROM emp
WHERE sal > 500000/num_tab(i);
EXCEPTION
WHEN dml_errors THEN
errors:= SQL%BULK_EXCEPTIONS.COUNT;
dbms_output.put_line('Number of errors is ' || errors);
FOR i IN 1..errors LOOP
dbms_output.put_line('Error' || i || ' occurred during '||'iteration' || SQL%BULK_EXCEPTIONS(i).ERROR_INDEX);
dbms_output.put_line('Oracle error is ' ||SQLERRM(-SQL%BULK_EXCEPTIONS(i).ERROR_CODE));
END LOOP;
END;
Dalam contoh ini, PL/SQL memunculkan predefined exception ZERO_DIVIDE ketika I sama dengan 2, 6, 10. Setelah bulk-bind selesai, SQL%BULK_EXCEPTIONS.COUNT menghasilkan 3, dan isi dari SQL%BULK_EXCEPTIONS adalah (2,1476, (6,1476), dan (10,1476). Untuk mendapatkan Oracle error message (yang mengandung code), kita menegasikan nilai SQL%BULK_EXCEPTIONS(i).ERROR_CODE dan melewatkan hasilnya ke fungsi error-reporting SQLERRM, yang mengharapkan angka negatif. Berikut ini outputnya:
Number of errors is 3
Error 1 occurred during iteration 2
Oracle error is ORA-01476: divisor is equal to zero
Error 2 occurred during iteration 6
Oracle error is ORA-01476: divisor is equal to zero
Error 3 occurred during iteration 10
Oracle error is ORA-01476: divisor is equal to zero
5.14. Menampilkan Hasil Query ke dalam Collections dengan Klausa BULK COLLECT
Kata kunci BULK COLLECT memberitahu SQL engine untuk melakukan bulk-bind output collections sebelum mengembalikannya ke PL/SQL engine. Kita dapat menggunakan kata-kata kunci di dalam klausa SELECT INTO, FETCH INTO, dan RETURNING INTO. Berikut ini adalah sintaksnya:
… BULK COLLECT INTO collection_name[, collection_name] …
SQL engine melakukan bulk-binds seluruh collections yang direferensi di dalam daftar INTO. Kolom-kolom terkait dapat menyimpan nilai scalar atau komposit termasuk objects. Dalam contoh berikut ini, SQL engine mengambil seluruh kolom-kolom empno dan ename ke dalam nested tables sebelum mengembalikan tables ke PL/SQL engine:
DECLARE
TYPE NumTab IS TABLE OF emp.empno%TYPE;
TYPE NameTab IS TABLE OF emp.ename%TYPE;
enums NumTab; -- tidak perlu diinisialisasi
names NameTab;
BEGIN
SELECT empno, ename BULK COLLECT
INTO enums, names
FROM emp;
...
END;
Dalam contoh berikutnya, SQL engine mengambil seluruh nilai-nilai di dalam kolom object ke dalam nested table sebelum mengembalikan table ke PL/SQL engine:
CREATE TYPE Coords AS OBJECT (x NUMBER, y NUMBER);
CREATE TABLE grid (num NUMBER, loc Coords);
INSERT INTO grid VALUES(10, Coords(1,2));
INSERT INTO grid VALUES(20, Coords(3,4));
DECLARE
TYPE CoordsTab IS TABLE OF Coords;
pairs CoordsTab;
BEGIN
SELECT loc BULK COLLECT
INTO pairs
FROM grid;
-- sekarang pasangannya berisi (1,2) dan (3,4)
END;
SQL engine menginisialisasi dan memperpanjang collections untuk kita. (Namun, ia tidak dapat memperpanjang varray melampaui ukuran maksimumnya). Kemudian, dimulai dari index 1, ia menambahkan elemen-elemen secara berurutan dan menimpa beberapa elemen-elemen pre-existent.
SQL engine melakukan bulk-binds seluruh kolom-kolom pada database. Sehingga, jika table memiliki 50,000 baris data, engine mengambil 50,000 nilai-nilai kolom ke dalam collection yang menjadi target. Namun, kita dapat menggunakan pseudocolumn ROWNUM untuk membatasi jumlah baris data yang diproses. Dalam contoh berikut ini, kita membatasi jumlah baris data menjadi 100.
DECLARE
TYPE SalList IS TABLE OF emp.sal%TYPE;
sals SalList;
BEGIN
SELECT sal BULK COLLECT
INTO sals
FROM emp
WHERE ROWNUM <= 100;
...
END;
5.14.1. Contoh dari Bulk Fetching dari Cursor Ke dalam Satu atau Lebih Collections
Kita dapat melakukan bulk-fetch dari cursor ke dalam satu atau lebih collections:
DECLARE
TYPE NameList IS TABLE OF emp.ename%TYPE;
TYPE SalList IS TABLE OF emp.sal%TYPE;
CURSOR c1 IS
SELECT ename, sal
FROM emp
WHERE sal > 1000;
names NameList;
sals SalList;
BEGIN
OPEN c1;
FETCH c1 BULK COLLECT INTO names, sals;
END;
Ke dalam Collection dari Records
Kita dapat melakukan bulk-fetch dari cursor ke dalam collection dari records:
DECLARE
TYPE DeptRecTab IS TABLE OF dept%ROWTYPE;
dept_recs DeptRecTab;
CURSOR c1 IS
SELECT deptno, dname, loc
FROM dept
WHERE deptno > 10;
BEGIN
OPEN c1;
FETCH c1 BULK COLLECT INTO dept_recs;
END;
5.14.2. Membatasi Baris Data untuk Operasi Bulk FETCH dengan Klausa LIMIT
Klausa opsional LIMIT, diperbolehkan hanya di dalam perintah-perintah FETCH bulk (bukan scalar), mengijinkan kita membatasi jumlah bari data yang diambil dari database. Sintaksnya adalah:
FETCH … BULK COLLECT INTO … [LIMIT rows];
dimana rows dapat berupa literal, variable, atau ekspresi namun harus berupa angka. Jika tidak, PL/SQL memunculkan predefined exception VALUE_ERROR. Jika angkanya tidak positif, PL/SQL memunculkan INVALID_NUMBER. Jika perlu, PL/SQL membulatkan keatas angka tersebut ke integer terdekat.
DECLARE
TYPE NumTab IS TABLE OF NUMBER INDEX BY BINARY_INTEGER;
CURSOR c1 IS
SELECT empno FROM emp;
empnos NumTab;
rows NATURAL := 10;
BEGIN
OPEN c1;
LOOP
/* Perintah berikut ini mengambil 10 baris data (atau kurang). */
FETCH c1 BULK COLLECT INTO empnos LIMIT rows;
EXIT WHEN c1%NOTFOUND;
...
END LOOP;
CLOSE c1;
END;
5.14.3. Menampilkan Hasil-hasil DML ke dalam Collection dengan Klausa RETURNING INTO
Kita dapat menggunakan klausa BULK COLLECT di dalam klausa RETURNING INTO dari perintah INSERT, UPDATE, atau DELETE, seperti ditunjukkan oleh contoh berikut ini:
DECLARE
TYPE NumList IS TABLE OF emp.empno%TYPE;
enums NumList;
BEGIN
DELETE
FROM emp
WHERE deptno = 20
RETURNING empno BULK COLLECT INTO enums;
-- jika terdapat lima karyawan di dalam departemen 20,
-- maka enums berisi lima nomor karyawan
END;
5.14.4. Batasan-batasan pada BULK COLLECT
Batasan-batasan berikut ini diterapkan pada klausa BULK COLLECT:
* Kita tidak dapat melakukan bulk collect ke dalam associative array yang memiliki kunci bertipe string.
* Kita dapat menggunakan klausa BULK COLLECT hanya pada program-program server-side (tidak pada client-side). Jika tidak, kita akan mendapatkan error this feature is not supported in client-side programs.
* Seluruh target-target di dalam klausa BULK COLLECT INTO harus berupa collections, seperti ditunjukkan oleh contoh berikut ini:
DECLARE
TYPE NameList IS TABLE OF emp.ename%TYPE;
names NameList;
salary emp.sal%TYPE;
BEGIN
SELECT ename, sal BULK COLLECT INTO names, salary -- pelanggaran target
FROM emp
WHERE ROWNUM < 50;
...
END;
* Target-target komposit (seperti objects) tidak dapat digunakan di dalam klausa RETURNING INTO. Jika tidak, kita akan mendapatkan error unsupported feature with RETURNING INTO clause.
* Ketika konversi-konversi tipe data implisit diperlukan, target-target komposit tidak dapat digunakan di dalam klausa BULK COLLECT INTO.
* Ketika konversi tipe data implisit diperlukan, collection dari target komposit (seperti collection dari objects) tidak dapat digunakan di dalam klausa BULK COLLECT INTO.
5.14.5. Menggunakan FORALL dan BULK COLLECT Secara Bersama-sama
Kita dapat mengkombinasikan klausa BULK COLLECT bersama perintah FORALL, dimana SQL engine melakukan bulk-binds terhadap nilai-nilai kolom secara menaik (incremental). Di dalam contoh berikut ini, jika collection dept memiliki 3 elemen, setiap yang mana menyebabkan 5 baris data dihapus, maka collection enum memiliki 15 elemen ketika perintah selesai diproses:
FORALL j IN depts.FIRST..depts.LAST
DELETE FROM emp
WHERE empno = depts(j)
RETURNING empno BULK COLLECT INTO enums;
Nilai-nilai kolom yang dihasilkan oleh setiap eksekusi ditambahkan ke nilai-nilai yang dihasilkan sebelumnya. (Dengan perulangan FOR, nilai-nilai sebelumnya akan ditimpa).
Kita tidak dapat menggunakan perintah SELECT…BULK COLLECT di dalam perintah FORALL. Jika tidak, kita akan mendapatkan error implementation restriction: cannot use FORALL and BULK COLLECT INTO together in SELECT statements.
5.14.6. Menggunakan Host Arrays Bersama Bulk Binds
Program-program client-side dapat menggunakan blok-blok PL/SQL tanpa nama (anonymous) untuk melakukan bulk-bind terhadap input dan output dari host arrays. Pada kenyataannya, hal ini merupakan cara yang paling efisien untuk melewatkan collections ke dan dari database server.
Host arrays dideklarasikan di dalam host environment seperti halnya program OCI atau Pro*C dan harus diawali dengan tanda titik dua (:) untuk membedakan mereka dengan collections PL/SQL. Di dalam contoh di bawah ini, input host arrays digunakan di dalam perintah DELETE. Pada saat runtime, blok PL/SQL tak bernama (anonymous) dikirimkan ke database server untuk eksekusi.
DECLARE
...
BEGIN
-- asumsikan bahwa nilai-nilai diberikan kepada host array
-- dan host variables di dalam the host environment
FORALL i IN :lower..:upper
DELETE FROM emp
WHERE deptno = :depts(i);
...
END;
5.15. Apa itu Record?
Sebuah record adalah kumpulkan item-item data yang saling berhubungan dan disimpan di dalam fields, dan masing-masing memiliki nama dan tipe data sendiri. Bayangkan kita memiliki berbagai macam data tentang karyawan seperti nama, gaji, dan tanggal masuk kerja. Item-item ini secara logikal berelasi namun tidak sama dalam hal tipe. Record yang mengandung field untuk setiap item mengijinkan kita untuk memperlakukan data sebagai unit logikal. Dengan demikian, records memudahkan kita untuk mengatur dan merepresentasikan informasi.
Attribute %ROWTYPE mengijinkan kita untuk mendeklarasikan record yang merepresentasikan baris data di dalam database table. Namun, kita tidak dapat menentukan tipe-tipe data untuk field-field di dalam record atau mendeklarasikan field-field milik kita sendiri. Tipe data RECORD menghilangkan batasan-batasan ini dan mengijinkan kita untuk mendefinisikan record-record kita sendiri.
5.15.1. Mendeklarasikan Records
Sekali kita mendefinisikan tipe RECORD, kita dapat mendeklarasikan records dengan tipe tersebut, seperti ditunjukkan oleh contoh berikut ini. Identifier item_info merepresentasikan keseluruhan record.
DECLARE
TYPE StockItem IS RECORD (item_no INTEGER(3), description VARCHAR2(50),
quantity INTEGER, price REAL(7,2));
item_info StockItem; -- mendeklarasikan record
BEGIN
...
END;
Seperti halnya variable-variable scalar, user-defined records (record-record yang didefinisikan sendiri oleh user) dapat dideklarasikan sebagai parameter-parameter formal dari procedures dan functions. Contohnya adalah sebagai berikut:
DECLARE
TYPE EmpRec IS RECORD (emp_id emp.empno%TYPE, last_name VARCHAR2(10),
job_title VARCHAR2(9), salary NUMBER(7,2));
...
PROCEDURE raise_salary (emp_info EmpRec);
BEGIN
...
END;
5.15.2. Menginisialisasi Records
Contoh di bawah ini menunjukkan bahwa kita dapat menginisialisasi record di dalam definisi tipe-nya. Ketika kita mendeklarasikan record dengan tipe TimeRec, ketiga field-nya mengasumsikan bahwa nilai awalnya adalah nol.
DECLARE
TYPE TimeRec IS RECORD (secs SMALLINT := 0, mins SMALLINT := 0, hrs SMALLINT := 0);
BEGIN
...
END;
Contoh berikutnya menunjukkan bahwa kita dapat memaksakan constraint NOT NULL terhadap field, sehingga mencegah pemberian nilai null terhadap field tersebut. Field-field yang dideklarasikan sebagai NOT NULL harus diinisialisasi.
DECLARE
TYPE StockItem IS RECORD (item_no INTEGER(3) NOT NULL := 999, description VARCHAR2(50),
quantity INTEGER, price REAL(7,2));
BEGIN
...
END;
5.15.3. Mereferensi Records
Tidak seperti elemen-elemen di dalam collection, yang diakses dengan menggunakan subscript-subscript, field-field di dalam record diakses melalui namanya. Untuk mereferensi field-field tunggal, kita menggunakan notasi titik dan sintaks berikut:
record_name.field_name
Sebagai contoh, kita mereferensi hire_date di dalam record emp_info seperti berikut:
emp_info.hire_date…
Ketika memanggil function yang menghasilkan user-defined record, kita menggunakan sintaks berikut ini untuk mereferensi field-field di dalam record:
function_name(parameter_list).field_name
Sebagai contoh, pemanggilan function nth_highest_sal berikut ini mereferensi field salary di dalam record emp_info:
DECLARE
TYPE EmpRec IS RECORD (emp_id NUMBER(4), job_title VARCHAR2(9), salary NUMBER(7,2));
middle_sal NUMBER(7,2);
FUNCTION nth_highest_sal (n INTEGER) RETURN EmpRec IS
emp_info EmpRec;
BEGIN
...
RETURN emp_info; -- menghasilkan record
END;
BEGIN
middle_sal:= nth_highest_sal(10).salary; -- memanggil function
...
END;
Ketika memanggil function tanpa parameter, kita menggunakan sintaks:
function_name().field_name - perhatikan parameter kosong
Untuk mereferensi nested fields di dalam record yang dihasilkan oleh function, kita menggunakan notasi titik tambahan. Sintaksnya:
function_name(parameter_list).field_name.nested_field_name
Untuk lebih jelasnya, pemanggilan function item berikut ini mereferensi kepada nested field minutes di dalam record item_info:
DECLARE
TYPE TimeRec IS RECORD (minutes SMALLINT, hours SMALLINT);
TYPE AgendaItem IS RECORD (priority INTEGER, subject VARCHAR2(100), duration TimeRec);
FUNCTION item (n INTEGER) RETURN AgendaItem IS
item_info AgendaItem;
BEGIN
...
RETURN item_info; -- menghasilkan record
END;
BEGIN
...
IF item(3).duration.minutes > 30 THEN ... -- memanggil function
END;
Juga, kita menggunakan notasi titik tambahan untuk mereferensi attributes dari object yang tersimpan di dalam field, seperti ditunjukkan oleh contoh berikut ini:
DECLARE
TYPE FlightRec IS RECORD (flight_no INTEGER, plane_id VARCHAR2(10),
captain Employee, -- mendeklarasikan object
passengers PassengerList, -- mendeklarasikan varray
depart_time TimeRec, -- mendeklarasikan nested record
airport_code VARCHAR2(10));
flight FlightRec;
BEGIN
...
IF flight.captain.name = 'H Rawlins' THEN ...
END;
5.15.4. Memberikan Nilai-nilai Null Terhadap Records
Untuk mengatur field-field di dalam record menjadi null, secara sederhana kita bisa memberikannya nilai dari record tak terinisialisasi dari tipe yang sama, seperti ditunjukkan oleh contoh berikut ini:
DECLARE
TYPE EmpRec IS RECORD (emp_id emp.empno%TYPE, job_title VARCHAR2(9),
salary NUMBER(7,2));
emp_info EmpRec;
emp_null EmpRec;
BEGIN
emp_info.emp_id:= 7788;
emp_info.job_title:= 'ANALYST';
emp_info.salary:= 3500;
emp_info:= emp_null; -- me-null-kan seluruh fields di dalam emp_info
...
END;
5.15.5. Memberikan Nilai Kepada Records
Kita dapat memberikan nilai dari ekspresi kepada field tertentu di dalam record dengan menggunakan sintaks berikut ini:
record_name.field_name := expression;
Di dalam contoh berikut ini, kita mengkonversi nama karyawan menjadi huruf besar:
emp_info.ename := UPPER(emp_info.ename);
Dibandingkan harus memberikan nilai-nilai secara terpisah kepada setiap field di dalam record, kita dapat memberikan nilai-nilai kepada seluruh field sekali saja. Hal ini dapat dilakukan dengan dua cara. Pertama, kita dapat memberikan satu user-defined record ke lainnya jika mereka memiliki tipe data yang sama. Memiliki field-field yang sesuai saja tidaklah cukup. Mari kita perhatikan contoh berikut ini:
DECLARE
TYPE DeptRec IS RECORD (dept_num NUMBER(2), dept_name VARCHAR2(14));
TYPE DeptItem IS RECORD (dept_num NUMBER(2), dept_name VARCHAR2(14));
dept1_info DeptRec;
dept2_info DeptItem;
BEGIN
...
dept1_info := dept2_info; -- tidak boleh; tipe-tipe data berbeda
END;
Seperti ditunjukkan oleh contoh berikutnya, kita dapat memberikan record %ROWTYPE kepada user-defined record jika field-field mereka sesuai di dalam jumlah dan urutannya, serta field-field terkait memiliki tipe-tipe data yang kompatibel:
DECLARE
TYPE DeptRec IS RECORD (dept_num NUMBER(2), dept_name VARCHAR2(14),
location VARCHAR2(13));
dept1_info DeptRec;
dept2_info dept%ROWTYPE;
BEGIN
SELECT *
INTO dept2_info
FROM dept
WHERE deptno = 10;
dept1_info:= dept2_info;
...
END;
Kedua, kita dapat menggunakan perintah SELECT atau FETCH untuk mengambil nilai-nilai kolom dan memberikannya ke record, seperti ditunjukkan oleh contoh di bawah ini. Kolom-kolom di dalam select-list harus tampil dengan urutan yang sama dengan field-field di dalam record kita.
DECLARE
TYPE DeptRec IS RECORD (dept_num NUMBER(2), dept_name VARCHAR2(14),
location VARCHAR2(13));
dept_info DeptRec;
BEGIN
SELECT *
INTO dept_info
FROM dept
WHERE deptno = 20;
...
END;
Namun, kita tidak dapat memberikan daftar nilai-nilai kepada record dengan menggunakan perintah pemberian nilai. Sintaks berikut ini tidak diperbolehkan:
record_name := (value1, value2, value3, …); — tidak diperbolehkan
Contoh di bawah ini menunjukkan kepada kita bahwa kita dapat memberikan satu nested record kepada lainnya jika mereka memiliki tipe data yang sama. Pemberian-pemberian nilai seperti ini diperbolehkan bahkan jika record-record yang melingkupinya memiliki tipe-tipe data yang berbeda.
DECLARE
TYPE TimeRec IS RECORD (mins SMALLINT, hrs SMALLINT);
TYPE MeetingRec IS RECORD (
day DATE,
time_of TimeRec, -- nested record
room_no INTEGER(4));
TYPE PartyRec IS RECORD (
day DATE,
time_of TimeRec, -- nested record
place VARCHAR2(25));
seminar MeetingRec;
party PartyRec;
BEGIN
...
party.time_of:= seminar.time_of;
END;
5.15.6. Membandingkan Records
Records tidak dapat diuji dalam hal null-nya, kesamaannya, atau ketidaksamaannya. Sebagai contoh, kondisi-kondisi IF berikut ini tidak diperbolehkan:
BEGIN
...
IF emp_info IS NULL THEN ... -- pelanggaran
IF dept2_info > dept1_info THEN ... -- pelanggaran
END;
5.16. Memanipulasi Records
Tipe data RECORD mengijinkan kita untuk mengumpulkan informasi mengenai attributes dari sesuatu. Informasi ini mudah dimanipulasi karena kita dapat mengacu kepada kumpulan tersebut sebagai keseluruhan. Di dalam contoh berikut ini, kita mengumpulkan data-data berbagai pos akuntansi dari database tables assets dan liabilities, lalu menggunakan analisa rasio untuk membandingkan performa dari dua cabang perusahaan.
DECLARE
TYPE FiguresRec IS RECORD (cash REAL, notes REAL, ...);
sub1_figs FiguresRec;
sub2_figs FiguresRec;
FUNCTION acid_test (figs FiguresRec) RETURN REAL IS ...
BEGIN
SELECT cash, notes, ...
INTO sub1_figs
FROM assets, liabilities
WHERE assets.sub = 1 AND liabilities.sub = 1;
SELECT cash, notes, ...
INTO sub2_figs
FROM assets, liabilities
WHERE assets.sub = 2 AND liabilities.sub = 2;
IF acid_test(sub1_figs) > acid_test(sub2_figs) THEN ...
...
END;
Perhatikan betapa mudahnya ia melewatkan pos-pos yang telah berhasil diambil ke function acid_test, yang mana menghitung rasio keuangan.
Dalam SQL*Plus, andaikata kita mendefinisikan object type Passenger, seperti berikut ini:
SQL> CREATE TYPE Passenger AS OBJECT(
2 flight_no NUMBER(3),
3 name VARCHAR2(20),
4 seat CHAR(5));
Berikutnya, kita mendefinisikan tipe VARRAY PassengerList, yang menyimpan objects Passenger:
SQL> CREATE TYPE PassengerList AS VARRAY(300) OF Passenger;
Akhirnya, kita menciptakan relational table flights, yang memiliki kolom dengan tipe PassengerList, seperti berikut ini:
SQL> CREATE TABLE flights (
2 flight_no NUMBER(3),
3 gate CHAR(5),
4 departure CHAR(15),
5 arrival CHAR(15),
6 passengers PassengerList);
Setiap item di dalam kolom passengers merupakan varray yang akan menyimpan daftar penumpang untuk penerbangan yang telah diberikan. Sekarang, kita dapat mempopulasikan database table flights, seperti berikut ini:
BEGIN
INSERT INTO flights
VALUES(109,'80', 'DFW 6:35PM', 'HOU 7:40PM',PassengerList(Passenger(109,'Paula Trusdale', '13C'),
Passenger(109,'Louis Jemenez', '22F'),Passenger(109,'Joseph Braun', '11B'), ...));
INSERT INTO flights
VALUES(114,'12B', 'SFO 9:45AM', 'LAX 12:10PM',PassengerList(Passenger(114,'Earl Benton', '23A'),
Passenger(114,'Alma Breckenridge', '10E'),Passenger(114,'Mary Rizutto', '11C'), ...));
INSERT INTO flights
VALUES(27,'34', 'JFK 7:05AM', 'MIA 9:55AM',PassengerList(Passenger(27,'Raymond Kiley', '34D'),
Passenger(27,'Beth Steinberg', '3A'),Passenger(27,'Jean Lafevre', '19C'), ...));
END;
Di dalam contoh di bawah ini, kita mengambil baris-baris data dari database table flights dan memasukkannya ke dalam record flight_info. Dengan cara itu, kita dapat memperlakukan seluruh informasi tentang penerbangan, termasuk daftar penumpang, sebagai unit logikal.
DECLARE
TYPE FlightRec IS RECORD (flight_no NUMBER(3), gate CHAR(5), departure CHAR(15),
arrival CHAR(15), passengers PassengerList);
flight_info FlightRec;
CURSOR c1 IS
SELECT *
FROM flights;
seat_not_available EXCEPTION;
BEGIN
OPEN c1;
LOOP
FETCH c1 INTO flight_info;
EXIT WHEN c1%NOTFOUND;
FOR i IN 1..flight_info.passengers.LAST LOOP
IF flight_info.passengers(i).seat = 'NA' THEN
dbms_output.put_line(flight_info.passengers(i).name);
RAISE seat_not_available;
END IF;
...
END LOOP;
END LOOP;
CLOSE c1;
EXCEPTION WHEN seat_not_available THEN
...
END;
5.16.1. Menambahkan PL/SQL Records ke dalam Database
Perluasan PL/SQL dari perintah INSERT mengijinkan kita untuk menambahkan data ke dalam baris-baris data pada database dengan menggunakan variable tunggal bertipe RECORD atau %ROWTYPE dibandingkan daftar fields. Hal ini membuat kode program kita lebih mudah dibaca dan lebih mudah dipelihara.
Jumlah fields di dalam record harus sama dengan jumlah kolom yang disebutkan di dalam klausa INTO, dan fields dan kolom-kolom terkait harus memiliki tipe data yang kompatibel. Untuk meyakinkan kita bahwa sebuah record kompatibel dengan table, mungkin lebih baik bagi kita untuk mendeklarasikan variable dengan tipe table_name%ROWTYPE.
5.16.1.1. Menambahkan PL/SQL Record Menggunakan %ROWTYPE: Contoh
Contoh ini mendeklarasikan variable record menggunakan pengkualifikasi %ROWTYPE. Kita dapat menambahkan variable ini tanpa menentukan daftar kolom. Deklarasi %ROWTYPE memastikan bahwa attribute-attribute record benar-benar memiliki nama dan tipe data yang sama dengan kolom-kolom pada table.
DECLARE
dept_info dept%ROWTYPE;
BEGIN
-- deptno, dname, and loc adalah kolom pada table.
-- Record mengambil nama-nama ini dari %ROWTYPE.
dept_info.deptno:= 70;
dept_info.dname:= 'PERSONNEL';
dept_info.loc:= 'DALLAS';
-- Menggunakan %ROWTYPE berarti kita dapat mengabaikan daftar kolom
-- (deptno, dname, loc) dari perintah INSERT.
INSERT INTO dept VALUES dept_info;
END;
5.16.2. Mengubah Database dengan Nilai-nilai PL/SQL Record
Perluasan PL/SQL dari perintah UPDATE mengijinkan kita untuk mengubah baris-baris data pada database dengan menggunakan variable tunggal bertipe RECORD atau %ROWTYPE dibandingkan dengan daftar fields.
Jumlah fields di dalam record harus sama dengan jumlah kolom yang disebutkan dalam klausa SET, dan field-field dan kolom-kolom yang berhubungan harus memiliki tipe data yang kompatibel.
Mengubah Baris Data Menggunakan: Contoh
Kita dapat menggunakan kata kunci ROW untuk merepresentasikan keseluruhan baris data:
DECLARE
dept_info dept%ROWTYPE;
BEGIN
dept_info.deptno:= 30;
dept_info.dname:= 'MARKETING';
dept_info.loc:= 'ATLANTA';
-- Baris data akan memiliki nilai untuk kolom
-- yang diisi, dan null untuk kolom-kolom lainnya.
UPDATE dept
SET ROW = dept_info
WHERE deptno = 30;
END;
Kata kunci ROW diperbolehkan hanya pada sisi kiri dari klausa SET.
SET ROW Tidak Diperbolehkan Bersama Subquery: Contoh
Kita tidak dapat menggunakan ROW bersama dengan subquery. Sebagai contoh, perintah UPDATE berikut ini tidak diperbolehkan:
UPDATE emp SET ROW = (SELECT * FROM mgrs); -- tidak diperbolehkan
Mengubah Baris Data Menggunakan Record yang Mengandung Object: Contoh
Records mengandung tipe-tipe objects diperbolehkan:
CREATE TYPE Worker AS OBJECT (name VARCHAR2(25), dept VARCHAR2(15));
/
CREATE TABLE teams (team_no NUMBER, team_member Worker);
DECLARE
team_rec
teams%ROWTYPE;
BEGIN
team_rec.team_no:= 5;
team_rec.team_member:= Worker('Paul Ocker', 'Accounting');
UPDATE teams SET ROW = team_rec;
END;
/
Mengubah Baris Data Menggunakan Record yang Mengandung Collection: Contoh
Record dapat pula mengandung collections:
CREATE TYPE Worker AS OBJECT (name VARCHAR2(25), dept VARCHAR2(15));
/
CREATE TYPE Roster AS TABLE OF Worker;
/
CREATE TABLE teams (team_no NUMBER, members Roster)
NESTED TABLE members STORE AS teams_store;
INSERT INTO teams
VALUES (1, Roster(
Worker('PaulOcker', 'Accounting'),
Worker('Gail Chan', 'Sales')
Worker('Marie Bello', 'Operations')
Worker('Alan Conwright', 'Research')));
DECLARE
team_rec teams%ROWTYPE;
BEGIN
team_rec.team_no:= 3;
team_rec.members:= Roster(
Worker('William Bliss', 'Sales'),
Worker('Ana Lopez', 'Sales')
Worker('Bridget Towner', 'Operations')
Worker('Ajay Singh', 'Accounting'));
UPDATE teams
SET ROW = team_rec;
END;
/
Menggunakan Klausa RETURNING Bersama Record: Contoh
Perintah-perintah INSERT, UPDATE, dan DELETE dapat terdiri dari klausa RETURNING, yang menghasilkan nilai-nilai kolom dari baris-baris data yang dihasilkan ke dalam variable record PL/SQL. Hal ini menyingkirkan kebutuhan untuk melakukan SELECT terhadap baris data setelah penambahan atau penghapusan, atau sebelum penghapusan. Kita dapat menggunakan klausa ini hanya ketika beroperasi pada satu baris data.
Di dalam contoh berikut ini, kita mengubah gaji karyawan dan, pada saat bersamaan, menampilkan nama, jabatan, dan gaji baru karyawan ke dalam variable record:
DECLARE
TYPE EmpRec IS RECORD (emp_name VARCHAR2(10), job_title VARCHAR2(9), salary NUMBER(7,2));
emp_info EmpRec;
emp_id NUMBER(4);
BEGIN
emp_id:= 7782;
UPDATE emp
SET sal = sal * 1.1
WHERE empno = emp_id
RETURNING ename, job, sal INTO emp_info;
END;
5.16.3. Batasan-batasan pada Penambahan/Pengubahan Record
Saat ini, batasan-batasan berikut ini diterapkan pada penambahan/pengubahan record:
* Variable-variable record diperbolehkan hanya dalam tempat-tempat berikut ini:
o Pada sisi kanan dari klausa SET di dalam perintah UPDATE
o Di dalam klausa VALUES dari perintah INSERT
o Di dalam sub-klausa INTO dari klausa RETURNING
Variable-variable record tidak diperbolehkan dalam daftar SELECT, klausa WHERE, klausa GROUP BY, atau klausa ORDER BY.
* Kata kunci ROW diperbolehkan hanya pada sisi kiri klausa SET. Juga, kita tidak dapat menggunakan ROW bersama subquery.
* Dalam perintah UPDATE, hanya klausa SET yang diperbolehkan jika ROW digunakan.
* Jika klausa VALUES dari perintah INSERT mengandung variable record, tidak ada variable atau nilai lain yang diperbolehkan di dalam klausa tersebut.
* Jika sub-klausa INTO dari klausa RETURNING mengandung variable record, tidak ada variable atau nilai yang diperbolehkan di dalam sub-klausa tersebut.
* Hal-hal berikut ini tidak di-support:
o Tipe-tipe nested record
o Functions yang menghasilkan record
o Penambahan/pengubahan record menggunakan perintah EXECUTE IMMEDIATE
5.16.4. Menampilkan Data ke dalam Collections dari Records
Operasi-operasi binding PL/SQL terbagai menjadi tiga kategori:
* define Mengacu kepada nilai-nilai database yang ditampilkan dengan perintah SELECT atau FETCH ke dalam PL/SQL variables atau host variables.
* in-bind Mengacu kepada nilai-nilai database yang ditambahkan dengan perintah INSERT atau dimodifikasi dengan perintah UPDATE.
* out-bind Mengacu kepada nilai-nilai database yang dihasilkan dengan klausa RETURNING dari perintah INSERT, UPDATE, atau DELETE ke dalam PL/SQL variables atau host variables.
PL/SQL mendukung bulk binding terhadap collections dari records di dalam perintah-perintah DML. Secara khusus, pendefinisian atau out-bind variable dapat berupa collection dari records, dan nilai-nilai in-bind dapat disimpan di dalam collection atau records. Sintaksnya adalah sebagai berikut:
SELECT select_items BULK COLLECT INTO record_variable_name
FROM rest_of_select_stmt
FETCH { cursor_name | cursor_variable_name | :host_cursor_variable_name}
BULK COLLECT INTO record_variable_name [LIMIT numeric_expression];
FORALL index IN lower_bound..upper_bound
INSERT INTO { table_reference|THE_subquery} [{column_name[, column_name]...}]
VALUES (record_variable_name(index)) rest_of_insert_stmt
FORALL index IN lower_bound..upper_bound
UPDATE {table_reference | THE_subquery} [alias]
SET (column_name[, column_name]...) = record_variable_name(index)
rest_of_update_stmt
RETURNING row_expression[, row_expression]...
BULK COLLECT INTO record_variable_name;
Di dalam setiap perintah dan klausa di atas, variable record menyimpan collection dari records. Jumlah fields di dalam record harus sama dengan jumlah item di dalam daftar SELECT, jumlah kolom di dalam klausa INSERT INTO, jumlah kolom di dalam klausa UPDATE…SET, atau jumlah baris ekspresi-ekspresi di dalam klausa RETURNING, secara berturut-turut. Field-field dan kolom-kolom yang berhubungan harus memiliki tipe-tipe data yang kompatibel. Berikut ini beberapa contohnya:
CREATE TABLE tab1 (col1 NUMBER, col2 VARCHAR2(20));
/
CREATE TABLE tab2 (col1 NUMBER, col2 VARCHAR2(20));
/
DECLARE
TYPE RecTabTyp IS TABLE OF tab1%ROWTYPE
INDEX BY BINARY_INTEGER;
TYPE NumTabTyp IS TABLE OF NUMBER
INDEX BY BINARY_INTEGER;
TYPE CharTabTyp IS TABLE OF VARCHAR2(20)
INDEX BY BINARY_INTEGER;
CURSOR c1 IS
SELECT col1, col2
FROM tab2;
rec_tab RecTabTyp;
num_tab NumTabTyp := NumTabTyp(2,5,8,9);
char_tab CharTabTyp := CharTabTyp('Tim', 'Jon', 'Beth', 'Jenny');
BEGIN
FORALL i IN 1..4
INSERT INTO tab1
VALUES(num_tab(i), char_tab(i));
SELECT col1, col2
BULK COLLECT INTO rec_tab
FROM tab1
WHERE col1 < 9;
FORALL i IN rec_tab.FIRST..rec_tab.LAST
INSERT INTO tab2
VALUES rec_tab(i);
FOR i IN rec_tab.FIRST..rec_tab.LAST LOOP
rec_tab(i).col1 := rec_tab(i).col1 + 100;
END LOOP;
FORALL i IN rec_tab.FIRST..rec_tab.LAST
UPDATE tab1 SET (col1, col2) = rec_tab(i) WHERE col1 < 8;
OPEN c1;
FETCH c1 BULK COLLECT INTO rec_tab;
CLOSE c1;
END;
hastinapura
Tidak ada komentar:
Posting Komentar