Oracle PL/SQL Programming, 2nd Edition

Oracle PL/SQL Programming, 2nd EditionSearch this book
Previous: 18.2 Oracle Objects ExampleChapter 18
Object Types
Next: 18.4 Manipulating Objects in PL/SQL and SQL
 

18.3 Syntax for Creating Object Types

This section explains the syntax for CREATE TYPE, CREATE TYPE BODY, and some of the other statements you will use when working with Oracle objects.

18.3.1 About Object Types

A given object type can have all of the following:

The default constructor, supplied automatically when you create an object type, allows you to create an object of the corresponding type. You have no direct control over this function (aside from how you have defined the attributes of the object type). The constructor is the only type of method that does not operate on an existing object.

Comparison methods are either MAP or ORDER methods (see Section 18.3.6, "Comparing Objects" later in this chapter). They allow you to establish rules so that SQL statements and PL/SQL programs can order, group, and otherwise compare object instances. Comparison methods are always functions.

Member methods are either member functions or member procedures. These are where programmers define the bulk of the object's behavior.

18.3.2 CREATE TYPE and DROP TYPE: Creating and Dropping Types

The CREATE TYPE statement has the following general format:

CREATE [ OR REPLACE ] TYPE <type name> AS OBJECT
   <attribute name> datatype, ...,
   MEMBER PROCEDURE | FUNCTION <procedure or function spec>, ...,
   [ MAP | ORDER MEMBER FUNCTION <comparison function spec>, ... ]
   [ PRAGMA RESTRICT_REFERENCES (<what to restrict>, restrictions) ]
);

As you would expect, you can drop a type using a DROP statement as follows:

DROP TYPE <type name> [ FORCE ] ;

Parameters have the following meanings:

OR REPLACE

Tells Oracle that you want to rebuild the type if it should happen to already exist. This will preserve grants. (See "Schema Evolution" later in the chapter for information about the effect this option has on the object type's metadata.)

type name

A legal Oracle identifier that isn't already in use by any other Oracle database object such as another type, table, or package. May be expressed in "schema dot" notation (e.g., SCOTT.foo).

attribute name

A legal PL/SQL identifier for the attribute.

datatype

Any legal Oracle datatype except LONG, LONG RAW, NCHAR, NCLOB, NVARCHAR2, ROWID, BINARY_INTEGER, BOOLEAN, PLS_INTEGER, RECORD, REF CURSOR, %TYPE, %ROWTYPE, or types that exist only within packages.

comparison function

Defines a function that allows comparison of object values.

what to restrict

This is either the name of the function or procedure, or the keyword DEFAULT. Using DEFAULT tells Oracle that all member functions and procedures in the object type will have the designated restrictions, without having to list each one in its own RESTRICT_REFERENCES pragma.

restrictions

One or more of the following: RNDS, WNDS, RNPS, and WNPS (see Chapter 17).

FORCE++

Tells Oracle that you want to drop a type even if there are other objects with dependencies on it. Even if you use FORCE, you can only drop a type if it has not been implemented in a table; you must first drop the table(s) before dropping the type.

Notice that the syntax for creating the specification is merely a comma-separated list of attributes and methods. There are no semicolons as you would find in a package specification.

You cannot impose NOT NULL or DEFAULT constraints at the attribute level. These constraints can, however, be applied to scalar attributes if you create an object table based on type. The syntax is:

CREATE TABLE <table name> OF <object type name>
   (<column constraint>, ... );

For example:

CREATE TABLE foos OF Foo_t
   (bar NOT NULL);

or, if you wish to name a constraint:

CREATE TABLE foos OF Foo_t
   (CONSTRAINT bar_not_null CHECK (bar IS NOT NULL));

18.3.3 CREATE TYPE BODY: Creating a Body

The syntax for the CREATE TYPE BODY statement is the following:

CREATE [ OR REPLACE ] TYPE BODY <type name> AS | IS (
   MEMBER PROCEDURE | FUNCTION <procedure or function body>, ...,
   [ MAP | ORDER MEMBER FUNCTION <comparison function body> ]
END;

Strictly speaking, type bodies are optional; you need a body only if you have created any methods in the specification. Similar to the rules for package specifications and bodies, the methods declared in the specification must match one for one the methods implemented in the body. Methods can be overloaded (see Chapter 15), and the standard rules about overloading apply.

18.3.4 Dot Notation

Even if you don't use the object extensions to Oracle, dot notation can be confusing. In SQL, for example, you may have references such as basil.meals.calories, referring to a column called calories in a meals table owned by basil. Add in remote database references, and you might get something like basil.meals.calories@mktg.ny.acme.com. In PL/SQL Version 2 and up, dots are found in record datatypes, table datatype operators, packaged procedure or function references, and elsewhere.

In the objects option, there are at least two new opportunities to get confused with dots: object data structures and object methods. (And the discussion below ignores the fact that object names can be preceded by the schema name, as in schema_name.object_name.)

18.3.4.1 Dots in data structures

In a PL/SQL program, you can refer to object attributes with dot notation, as in object_name.attribute_name. For example, after declaring and initializing an object my_pet of type Pet_t, we can do this:

IF my_pet.sex = 'M' THEN...

This variable means "the sex attribute of the object instance my_pet."

Referring to nested objects in PL/SQL using dot notation is almost intuitive, as long as you're using embedded objects (that is, the attribute is an object itself, not a REF to an object).

CREATE OBJECT Pet_t (
   ...
   owner Person_t,     -- embedded object, not a REF
   ...);

DECLARE
   the_dalmatian Pet_t;
BEGIN
   ...
   IF the_dalmatian.owner.first_name = 'Persephone'
   THEN...

The IF test above simply checks whether the first name of the owner of the Dalmatian is Persephone. In SQL statements, you can also use dots to navigate the components of nested objects. Even when you have nested objects with REFs, SQL graciously allows you to navigate to the referenced object without actually doing a join:

CREATE OBJECT Pet_t (
   ...
   owner_ref REF Person_t,
   ...);

CREATE TABLE pets of Pet_t;

SELECT name, p.owner_ref.first_name
  FROM pets p;

That's a pretty neat trick. No ugly join clause, just an intuitive "do the right thing" call. It works for attributes and member functions that are defined with the appropriate RESTRICT_REFERENCES pragma. But what do we do in PL/SQL? Is this legal?

DECLARE
   the_dalmatian Pet_t;
BEGIN
   SELECT VALUE(p) INTO the_dalmatian
     FROM pets p
    WHERE name = 'Cerberus';...
   IF the_dalmatian.owner_ref.first_name = 'Persephone'   -- invalid
   THEN... 

It won't work! In Oracle 8.0.3, you cannot navigate the database through PL/SQL REF variables. Repeat this to yourself like a mantra. Dot notation doesn't help us in this case. For now, you can instead use DEREF, described in detail later on; a future version of Oracle will likely include a built-in package called UTL_REF that supports navigation in PL/SQL.

18.3.4.2 Dots in method invocations

When you invoke an object's member function or procedure, the dot syntax is straightforward, as in the following:

object_instance_name.function_name (args)
object_instance_name.procedure_name (args)

If you want to use the output from one method as the input to another, you don't have to use a temporary variable. You can actually chain methods together with dots, as long as they are type compatible:

object_name.function_name(args).function_name(args).procedure_name(args)

Before we can take a look at an example that chains our Pet_t methods, we'll want to change the specification of print_me. Instead of using the default IN OUT mode of the SELF parameter in a member procedure, we are going to make it an IN. That is, instead of:

MEMBER PROCEDURE print_me

we want to use:

MEMBER PROCEDURE print_me (SELF IN Pet_t)

(Remember that we have to make this change in both the object type specification and the object type body.)

Why did we make the change? The default IN OUT mode can only accept a SELF parameter that is writeable, and function return values are never writeable. But as an IN-only parameter, SELF can now accept a Pet_t object that is returned from one of the other functions.

DECLARE
   the_pet Pet_t := Pet_t(1949,'Godzilla','BIG MONKEY','M',
      NULL,NULL,NULL);
BEGIN
   the_pet.set_tag_no(1948).set_photo('gz105.jpg').print_me();
END;

This means "change the tag number of the pet variable to 1948, change its photo to gz105.jpg, and print the result." If you give a little thought to the implications of this convenience feature, you'll realize that it could be valuable to define member functions which return the base object type, so that you can chain them together later.

Here are some rules about chaining:

  • Methods are invoked in order from left to right.

  • The return value of a chained method must be of the object type expected by the method to its right.

  • A chained call can include at most a single procedure.

  • If your chained call includes a procedure, it must be the right-most method in the chain.

  • Be sure that you don't try to use a function's return value (which is read-only) as an IN OUT input to the next method in the chain.

18.3.4.3 Attribute or method?

In PL/SQL, there is no automatic visual distinction between an object attribute and an object method unless the method has arguments. That is, in this code fragment:

IF my_pet.whatever = 'a value' THEN...

we can't immediately determine if "whatever" is an attribute or a method! In some cases, this ambiguity could be a feature, since one day we might want to replace an attribute by a method of the same name.

If we want to make our code less mysterious, we can add a trailing empty parameter list to method calls which have no parameters, as in the following:

my_pet.print_me();

The empty parentheses notation works for both member functions and member procedures.

NOTE: The situation is different in SQL statements. If you call a member function without parameters in a SQL statement, you must use empty parentheses notation. That is, if somefun is a function, don't do this:

SELECT p.somefun FROM pets p;  -- invalid

The statement above fails with an ORA-00904, "invalid column name." The correct syntax is:

SELECT p.somefun() FROM pets p;

18.3.5 SELF: The Implied Parameter

Because a method can only be called within the context of a particular object instance, it always has an object of the corresponding type as a "parameter." This makes sense because the method will (almost) always need access to that object's attributes. This implied parameter is called SELF. By default, SELF is an IN parameter in member functions, and an IN OUT parameter in member procedures.

If we create an object to hold American Kennel Club papers:

CREATE TYPE Akc_paper_t AS OBJECT(
   pet_ref REF Pet_t,
   issued_on DATE,
   contents BLOB);

the following member function specifications are equivalent:

MEMBER FUNCTION print_me RETURN BOOLEAN;
MEMBER FUNCTION print_me (SELF Akc_paper_t) RETURN BOOLEAN;
MEMBER FUNCTION print_me (SELF IN Akc_paper_t) RETURN BOOLEAN;

Similarly, member procedure SELF parameters default to IN OUT, so the following are equivalent to one another:

MEMBER PROCEDURE reissue;
MEMBER PROCEDURE reissue (SELF Akc_paper_t);
MEMBER PROCEDURE reissue (SELF IN OUT Akc_paper_t);

Within the object type body, you can refer to the SELF object explicitly; if you do not, PL/SQL name resolution rules will attempt to "do the right thing" with attribute references. In the example below, the name and issued_on attributes will resolve to attribute values even without the SELF parameter:

CREATE TYPE BODY Akc_paper_t
AS
   MEMBER FUNCTION print_me RETURN BOOLEAN
   IS
   BEGIN
      DBMS_OUTPUT.PUT_LINE('Name     : ' || name);
      DBMS_OUTPUT.PUT_LINE('Issued On: ' || issued_on);
      ...
   END;
END;

The PUT_LINE statements above are equivalent to:

      DBMS_OUTPUT.PUT_LINE('Name     : ' || SELF.name);
      DBMS_OUTPUT.PUT_LINE('Issued On: ' || SELF.issued_on);

NOTE: Including SELF explicitly can improve program clarity.

18.3.5.1 Forward type definitions

What would you do if you wanted to define object types that depend on each other? Suppose that we want to implement the following relationships:

  • Each pet has an owner of type Person_t; owners can have one or more pets.

  • A person can have one and only one favorite pet.

The solution is a forward type definition, similar to forward declarations in PL/SQL packages (see Chapter 16). A forward definition allows you to declare your intention to create a type before you actually define it:

/* Here is the incomplete type definition */
CREATE TYPE Person_t;

/* Now owner_ref can make a "forward" reference to the
|| Person_t type
*/
CREATE TYPE Pet_t AS OBJECT (
   tag_no INTEGER,
   owner_ref REF Person_t,
   ...the rest of the attributes and methods...
);
/* Now we can complete the type definition we started
|| earlier.
*/
CREATE TYPE Person_t AS OBJECT (
   name VARCHAR2(512),
   favorite_pet REF Pet_t,
   ...
);

If you want to create a recursive type, that is, one which refers to itself, a forward type definition is not required. For example, the Relative Pets organizational hierarchy might be implemented with recursion:

CREATE TYPE organization_unit_t AS OBJECT (
   id NUMBER,
   parent REF organization_unit_t
      -- works fine without forward type def
);

18.3.6 Comparing Objects

In the "old days," when Oracle offered only scalar datatypes, the semantics for comparing values were clearly defined. For example, columns of type NUMBER are easily compared, ordered, and grouped. Ditto for dates, and even character types, despite differences in national language sorting conventions. NULLs have always given us some grief, but we can't argue that the rules about them were vague. Things got a little more interesting in PL/SQL programs, because there we can have complex data structures such as records and table datatypes, which offer very few comparison features within the language.

Now, if we are taking an object-oriented approach, it would be useful if Oracle allowed statements such as the following:

IF my_pet > your_pet THEN ... -- my_pet and your_pet are objects

SELECT ... FROM pets ORDER BY owner;   -- owner is an object column

But it is not at all obvious how Oracle would deal with statements like these. Should it do some sort of "munching" average on the objects' attributes, or what?

In fact, Oracle allows us to formulate our own comparison rules for the object types we create. By defining a special MAP or ORDER member function when we define an object type, we can tell Oracle how to compare objects of that type in both PL/SQL and SQL expressions.

18.3.6.1 The MAP and ORDER methods

Let's say that we have created an object type Appointment_t that will help us in scheduling visits to the veterinary offices of Relative Pets. We might need an application to compare appointments:

DECLARE
   my_appointment Appointment_t;
   your_appointment Appointment_t;
BEGIN
   ...initialize the appointments...

   IF my_appointment > your_appointment THEN ...

To perform this greater-than comparison, you'll need to define either a MAP or an ORDER function. MAP and ORDER methods are mutually exclusive; a given object type may have exactly one MAP method, or exactly one ORDER method (or zero comparison methods of either type).

18.3.6.1.1 MAP member functions

The MAP method simply translates or "maps" each object into a scalar datatype space that Oracle knows how to compare. For example, suppose we had a simple rule that says appointments are "greater than" others if they occur later in time. Then the MAP method is trivial:

CREATE TYPE Appointment_t AS OBJECT (
   pet REF Pet_t,
   scheduled_date DATE,
   with_whom REF Doctor_t,
   MAP MEMBER FUNCTION compare RETURN DATE
);

CREATE TYPE BODY Appointment_t
AS
   MAP MEMBER FUNCTION compare RETURN DATE
   IS
   BEGIN
      RETURN scheduled_date;
   END compare;
END;

MAP functions accept no parameters and must return a date, character, or number -- that is, something that SQL and PL/SQL already know how to compare.

18.3.6.1.2 ORDER member functions

The alternative to MAP is an ORDER member function, which accepts two objects: SELF and another object of the same type. You must program the ORDER member function to return an INTEGER that is one of the values -1, 0, or 1, indicating the ordering relationship of the second object to SELF. That is, if you want:

  • SELF < second object, return -1

  • SELF = second object, return 0

  • SELF > second object, return +1

  • Undefined comparison, return NULL.

Let's look at an example of this type of function:

CREATE TYPE Location_t AS OBJECT (
   latitude REAL,
   longitude REAL,
   altitude REAL,
   ORDER MEMBER FUNCTION compare (the_location IN Location_t) 
      RETURN INTEGER
);

CREATE TYPE BODY Location_t
AS
   ORDER MEMBER FUNCTION compare (the_location IN Location_t)
      RETURN INTEGER
   IS
   -- A very lame attempt at comparing geographic locations
   BEGIN
      IF the_location.latitude = SELF.latitude
         AND the_location.longitude = SELF.longitude
         AND the_location.altitude = SELF.altitude THEN
            RETURN 0;
      ELSIF SELF.latitude > the_location.latitude
         OR SELF.longitude > the_location.longitude
         OR SELF.altitude > the_location.altitude THEN
            RETURN 1;
      ELSE
         RETURN -1;
      END IF;
   END;
END;

This ORDER member function will allow us to make simple comparisons such as:

IF location1 > location2 THEN
   plant_a_flag;
END IF:

Although not recommended, your ORDER method can return NULL under certain situations, and the object comparison itself will evaluate to NULL. That is, if our object type body were rewritten as follows:

CREATE TYPE BODY Location_t
AS
   ORDER MEMBER FUNCTION compare (the_location IN Location_t)
      RETURN INTEGER
   IS
   -- An even more lame attempt at comparing geographic locations
   BEGIN
      IF the_location.latitude = SELF.latitude
         AND the_location.longitude = SELF.longitude
         AND the_location.altitude = SELF.altitude THEN
            RETURN 0;
      ELSE
         RETURN NULL;
      END IF;
   END;
END;

Then, if attributes of two locations are equal, the expression (location1 = location2) will evaluate to TRUE; but if any of the attributes differ, then you can detect the condition using the IS NULL operator. Using the second version of the Location_t body, the expression below will always be true!

IF (location1 < location2) IS NULL THEN...

Suffice it to say that returning NULL from a comparison function is not particularly helpful.

There is nothing magic about the name you give the MAP and ORDER functions. In fact, other than in the type definition statements, you may never refer to this name. An added bonus of using MAP or ORDER functions is that they enable you to do things like ORDER BY and GROUP BY the object in SQL statements.

Which should you use -- MAP or ORDER? To some extent, it's a matter of what makes sense to your application, but keep in mind the following restrictions and qualifications:

  • A MAP method is more efficient than the equivalent ORDER method.

  • If you plan to perform hash joins on the object in SQL, you must use MAP, because this type of join requires a value to hash.

  • A MAP method is particularly appropriate if you are sequencing a large series of objects, while an ORDER method is more useful if you are comparing two objects.

18.3.6.2 Equality comparisons

If you don't create a MAP or ORDER method, Oracle allows you to test only for equality of two different objects. Two Oracle objects are "equal" if and only if they (1) are of the same object type; and (2) both have attributes with identical values. Object attributes get compared one at a time, in order, and the testing stops when the first mismatch is discovered.

Here is an example of testing for equality:

DECLARE
   the_1997_spec Marketing_spec_t;
   the_1998_spec Marketing_spec_t;
BEGIN
   ...
   IF the_1997_spec = the_1998_spec THEN ...

Or, if we had one table of marketing specs per year:

CREATE TABLE marketing_1997 OF Marketing_spec_t;
CREATE TABLE marketing_1998 OF Marketing_spec_t;

then we could compare from within SQL by using the VALUE operator:

SELECT s97.make, s97.model
 FROM marketing_1997 s97,
      marketing_1998 s98
WHERE VALUE(s97) = VALUE(s98);

NOTE: Default equality comparisons work only if the object table contains attributes that Oracle knows how to compare. For example, they will work on objects with scalar attributes, but they will not work on objects with collection attributes, embedded object types, REFs, or LOBs. Also, if you create a MAP or ORDER member function, you override Oracle's ability to perform the default equality test by comparing all the attributes.

18.3.7 Privileges

While there are two categories of users to whom object privileges may be granted, programmers and end users, there is only one Oracle privilege that applies to object types: EXECUTE. Let's look at how this privilege applies to DDL (typically for programmers) and DML (typically for end users).

18.3.7.1 DDL

Let's say that you are the Oracle user named SCOTT and you have created an object type Pet_t. You want to grant JOE permission to use this type in his own PL/SQL programs or tables. All you need to do is grant the EXECUTE privilege to him:

GRANT EXECUTE on Pet_t TO JOE;

Joe can then refer to the type using schema.type notation:

CREATE TABLE my_pets OF SCOTT.PET_T;

DECLARE
  the_pet SCOTT.PET_T;

EXECUTE privileges are also required by users who simply need to run PL/SQL anonymous blocks that use the object type.

18.3.7.2 DML

For object tables, the traditional SELECT, INSERT, UDPATE, and DELETE privileges still have meaning. A user with SELECT on the object table may only retrieve the relational columns and not the object-as-object. That is, he cannot use the VALUE operator. Similarly, the other three privileges, INSERT, UPDATE, and DELETE, apply only to the relational interpretation of the table.

In the same fashion, the grantee does not have permission to use the constructor or other object methods unless the object type owner has granted the user EXECUTE privilege on the object type.

18.3.7.3 Rights model

Suppose that the owner of a package grants me EXECUTE privileges on it in Oracle7. Whenever I execute the package, I am actually using the owner's privileges on tables, views, and the like. I need no privileges on the underlying structures. This definer rights model can be very useful in encapsulating the table data and protecting it from change except through the package.

As mentioned earlier in the chapter (see the Sidebar called "Encapsulation of Persistent Objects in Oracle"), the owner rights model may have a negative impact on object reuse, and it's conceivable that an object-relational database like Oracle could implement an invoker rights model for object methods. As with all new technology, we will simply have to wait and see whether such a change comes about, and if it does, what sort of impact it will have on existing applications.


Previous: 18.2 Oracle Objects ExampleOracle PL/SQL Programming, 2nd EditionNext: 18.4 Manipulating Objects in PL/SQL and SQL
18.2 Oracle Objects ExampleBook Index18.4 Manipulating Objects in PL/SQL and SQL

The Oracle Library Navigation

Copyright (c) 2000 O'Reilly & Associates. All rights reserved.

Library Home Oracle PL/SQL Programming, 2nd. Ed. Guide to Oracle 8i Features Oracle Built-in Packages Advanced PL/SQL Programming with Packages Oracle Web Applications Oracle PL/SQL Language Pocket Reference Oracle PL/SQL Built-ins Pocket Reference