| Home
RSS 2001 2002 2003 2004 2005 2006 2007 Letras Libros Pensar Cosas |
Foreign KeysThis page explains how to address database constraints in FireBird. Database constraints are constraints that span more than one table. The most common kind of constraint is the foreign key constraint. By using this constraint, we declare that certain values in a given row must exist in a row in another table. The simplest way to use these constraints is with a single value. The following sample SQL script can be used with FireBird's isql program ("%programfiles%\Firebird\Firebird_1_5\bin\isql.exe") to exemplify this. CREATE DATABASE 'c:/db-constraints.gdb' USER 'SYSDBA' PASSWORD 'masterkey'; CREATE TABLE CUSTOMERS ( CUSTOMER_ID INTEGER NOT NULL PRIMARY KEY, CUSTOMER_NAME VARCHAR(128) DEFAULT '(UNKNOWN)' NOT NULL ); CREATE TABLE ORDERS ( ORDER_ID INTEGER NOT NULL PRIMARY KEY, CUSTOMER_ID INTEGER NOT NULL REFERENCES CUSTOMERS, DELIVERED CHAR(1) DEFAULT 'T' NOT NULL CHECK (DELIVERED = 'T' OR DELIVERED = 'F') ); We can try our model out now and see what happens. INSERT INTO CUSTOMERS (CUSTOMER_ID, CUSTOMER_NAME) VALUES (1, 'JOHN'); INSERT INTO CUSTOMERS (CUSTOMER_ID, CUSTOMER_NAME) VALUES (2, 'MARY'); /* Customer ID = 1 exists in CUSTOMERS */ INSERT INTO ORDERS (ORDER_ID, CUSTOMER_ID, DELIVERED) VALUES (1, 1, 'T'); /* Customer ID = 3 does not exist in CUSTOMERS */ INSERT INTO ORDERS (ORDER_ID, CUSTOMER_ID, DELIVERED) VALUES (2, 3, 'T'); Statement failed, SQLCODE = -530 violation of FOREIGN KEY constraint "INTEG_7" on table "ORDERS" As you can see, the database server prevents us from inserting an order without a valid customer - all is well in business-land. There is another syntax, more verbose, that can be used when a given list of values must match all together in a row in a given table. Note that in this new example, we will name our constraint, to make it easier to understand what we're doing wrong if we get a constraint violation. The syntax is quite similar to the syntax used for a primary key constraint referring to multiple columns.
/* This table has a row with total sales for a given product
* in a given date. */
CREATE TABLE DAILY_PRODUCT_SALES (
DATE_OF_SALE DATE NOT NULL,
PRODUCT_ID INTEGER NOT NULL,
UNITS_SOLD INTEGER NOT NULL CHECK (UNITS_SOLD >= 0),
CONSTRAINT UNIQUE_DAILY_SALES
PRIMARY KEY (DATE_OF_SALE, PRODUCT_ID)
);
/* This table has a row indicating which customer bought a certain
* product on a given date. */
CREATE TABLE DAILY_PRODUCT_SALE_DETAILS (
DATE_OF_SALE DATE NOT NULL,
PRODUCT_ID INTEGER NOT NULL,
CUSTOMER_ID INTEGER NOT NULL REFERENCES CUSTOMERS,
CONSTRAINT UNIQUE_DAILY_SALE_DETAIL
PRIMARY KEY (PRODUCT_ID, DATE_OF_SALE, CUSTOMER_ID),
CONSTRAINT DAILY_SALE_DETAIL_AGGREGATED
FOREIGN KEY (DATE_OF_SALE, PRODUCT_ID)
REFERENCES DAILY_PRODUCT_SALES(DATE_OF_SALE, PRODUCT_ID)
ON DELETE CASCADE ON UPDATE CASCADE
);
/* Insert some products. Note that DATE_OF_SALE and PRODUCT_ID
* need to be unique *in combination*. */
INSERT INTO DAILY_PRODUCT_SALES VALUES ('2004-09-10', 1, 10);
INSERT INTO DAILY_PRODUCT_SALES VALUES ('2004-09-11', 1, 12);
INSERT INTO DAILY_PRODUCT_SALES VALUES ('2004-09-12', 1, 14);
INSERT INTO DAILY_PRODUCT_SALES VALUES ('2004-09-10', 2, 30);
INSERT INTO DAILY_PRODUCT_SALES VALUES ('2004-09-11', 2, 40);
INSERT INTO DAILY_PRODUCT_SALES VALUES ('2004-09-10', 3, 100);
/* Now, see how the table with the reference constraint works. */
INSERT INTO DAILY_PRODUCT_SALE_DETAILS VALUES ('2004-09-10', 1, 1);
INSERT INTO DAILY_PRODUCT_SALE_DETAILS VALUES ('2004-09-10', 1, 2);
/* Let's try a pair of values that exist by themselves, but
* not in combination. */
INSERT INTO DAILY_PRODUCT_SALE_DETAILS VALUES ('2004-09-12', 3, 2);
Statement failed, SQLCODE = -530
violation of FOREIGN KEY constraint "DAILY_SALE_DETAIL_AGGREGATED"
on table "DAILY_PRODUCT_SALE_DETAILS"
Just for kicks, I also threw in the ON DELETE CASCADE and ON UPDATE CASCADE clauses. They work by doing pretty much what you would expect.
SELECT * FROM DAILY_PRODUCT_SALE_DETAILS WHERE PRODUCT_ID = 1 AND DATE_OF_SALE = '2004-09-10'; DATE_OF_SALE PRODUCT_ID CUSTOMER_ID ============ ============ ============ 2004-09-10 1 1 2004-09-10 1 2 DELETE FROM DAILY_PRODUCT_SALES WHERE PRODUCT_ID = 1 AND DATE_OF_SALE = '2004-09-10'; SELECT * FROM DAILY_PRODUCT_SALE_DETAILS WHERE PRODUCT_ID = 1 AND DATE_OF_SALE = '2004-09-10'; [nothing] Check ClausesAnother options we have to declare database-wide constraints is to use the CHECK clause on tables and columns. The following two examples illustrate how these can be used.
/* We create a table of preferred customers. These customers
* must have bought from us three or more times. */
CREATE TABLE PREFERRED_CUSTOMERS (
CUSTOMER_ID INTEGER NOT NULL
CONSTRAINT PREFERRED_CUSTOMER_EXISTS REFERENCES CUSTOMERS,
CONSTRAINT CUSTOMER_PLACED_MANY_ORDERS
CHECK (3 <= (
SELECT COUNT(*)
FROM DAILY_PRODUCT_SALE_DETAILS D
WHERE D.CUSTOMER_ID = CUSTOMER_ID)),
PREFERRED_STATUS_FROM DATE DEFAULT 'NOW' NOT NULL
);
INSERT INTO PREFERRED_CUSTOMERS (CUSTOMER_ID)
VALUES (1);
Statement failed, SQLCODE = -297
Operation violates CHECK constraint CUSTOMER_PLACED_MANY_ORDERS on view or table
PREFERRED_CUSTOMERS
/* Reinsert the information we deleted and add more. */
INSERT INTO DAILY_PRODUCT_SALES VALUES ('2004-10-01', 1, 100);
INSERT INTO DAILY_PRODUCT_SALES VALUES ('2004-10-02', 1, 100);
INSERT INTO DAILY_PRODUCT_SALES VALUES ('2004-10-03', 1, 100);
INSERT INTO DAILY_PRODUCT_SALE_DETAILS VALUES ('2004-10-01', 1, 1);
INSERT INTO DAILY_PRODUCT_SALE_DETAILS VALUES ('2004-10-02', 1, 1);
INSERT INTO DAILY_PRODUCT_SALE_DETAILS VALUES ('2004-10-03', 1, 1);
INSERT INTO PREFERRED_CUSTOMERS (CUSTOMER_ID)
VALUES (1);
SELECT * FROM PREFERRED_CUSTOMERS;
CUSTOMER_ID PREFERRED_STATUS_FROM
============ =====================
1 2004-09-08
However, note that FireBird, like other database engines, may not be too smart about these checks...
/* Delete something that will invalidate the check. */
DELETE FROM DAILY_PRODUCT_SALE_DETAILS
WHERE PRODUCT_ID = 1 AND DATE_OF_SALE = '2004-10-01';
SELECT PC.CUSTOMER_ID, COUNT(*)
FROM PREFERRED_CUSTOMERS PC, DAILY_PRODUCT_SALE_DETAILS DPSD
WHERE PC.CUSTOMER_ID = DPSD.CUSTOMER_ID
GROUP BY PC.CUSTOMER_ID;
CUSTOMER_ID COUNT
============ ============
1 2
/* Whoops! The customer is still a preferred customer, violating the check. */
Unfortunately, these row constraints are not enforced at every possibility of table violation. Sometimes these problems can be addressed by using more structured checks from the client side, or offline processing techniques if required. ProblemNow, let's tackle a problem that cannot be solved with checks at all. Let's suppose we have the following classic ORDER_ITEMS table.
CREATE TABLE ORDER_ITEMS (
ORDER_ID INTEGER NOT NULL,
PART_ID INTEGER NOT NULL,
QUANTITY INTEGER NOT NULL CHECK (QUANTITY > 0),
CONSTRAINT ORDER_ITEM_IN_ORDER
FOREIGN KEY(ORDER_ID) REFERENCES ORDERS(ORDER_ID),
CONSTRAINT UNIQUE_ORDER_ITEM PRIMARY KEY(ORDER_ID, PART_ID)
);
We can try our model out now and see what happens.
INSERT INTO ORDERS (ORDER_ID, DELIVERED) VALUES (1, 'T');
INSERT INTO ORDER_ITEMS (
ORDER_ID, PART_ID, QUANTITY
) VALUES (
1, 100, 3
);
INSERT INTO ORDER_ITEMS (
ORDER_ID, PART_ID, QUANTITY
) VALUES (
1, 101, 1
);
SELECT O.ORDER_ID, COUNT(OI.PART_ID) AS ITEM_COUNT
FROM ORDERS O, ORDER_ITEMS OI
WHERE O.ORDER_ID = OI.ORDER_ID
GROUP BY O.ORDER_ID;
ORDER_ID ITEM_COUNT
============ ============
1 2
Now, let's suppose that we want to specify that orders must have order items to be valid. Fairly reasonable. We could declare a foreign key from ORDERS(ORDER_ID) to ORDER_ITEMS(ORDER_ID). However, the index on ORDER_ITEMS is not unique, so we get the following error. /* Commit because there is an active transaction referring to the * old schema. */ COMMIT; /* Add the constraint to the table. */ ALTER TABLE ORDERS ADD CONSTRAINT ORDER_HAS_ITEMS FOREIGN KEY(ORDER_ID) REFERENCES ORDER_ITEMS(ORDER_ID); Statement failed, SQLCODE = -607 unsuccessful metadata update -could not find UNIQUE INDEX with specified columns In any case, the problem is that the DBMS would try to enforce the constraint as soon as a new record is inserted into either table, and of course this would fail because there is no record inserted yet in the other table. In an ideal world, the DBMS would just wait until we tried to commit the transaction, and then verify all the constraints in the most efficient manner possible (perhaps deferring failed constraints somehow). In any case, we're still not there yet, but we can implement some of these things on our own. Oracle8 introduces this capability for the Oracle line of DBMSs. So, the first problem is how to track what constraints need to be enforced. A simple way to do this is to rely on one foreign key constraint in the database (which means that we still need to do inserts in a specific order), and then add a trigger to log that we need to verify the reverse constraint.
/* We accept duplicates in this table, but for all purposes
* they are merged into a single value. */
CREATE TABLE ORDERS_TO_CHECK (
ORDER_ID INTEGER NOT NULL
);
CREATE EXCEPTION ORDERS_REQUIRE_ITEMS
'Orders require order items to be stored in the database.';
SET TERM !!;
CREATE PROCEDURE CHECK_ORDERS
AS
DECLARE VARIABLE ORDER_ID INTEGER;
DECLARE VARIABLE ORDER_COUNT INTEGER;
DECLARE VARIABLE ORDER_ITEMS_COUNT INTEGER;
BEGIN
FOR
SELECT DISTINCT ORDER_ID
FROM ORDERS_TO_CHECK
INTO :ORDER_ID
DO BEGIN
SELECT COUNT(*)
FROM ORDER_ITEMS
WHERE ORDER_ID = :ORDER_ID
INTO :ORDER_ITEMS_COUNT;
IF (ORDER_ITEMS_COUNT = 0) THEN BEGIN
/* May have been deleted. */
SELECT COUNT(*)
FROM ORDERS
WHERE ORDER_ID = :ORDER_ID
INTO :ORDER_COUNT;
IF (ORDER_COUNT <> 0) THEN BEGIN
EXCEPTION ORDERS_REQUIRE_ITEMS;
END
END ELSE BEGIN
DELETE
FROM ORDERS_TO_CHECK
WHERE ORDER_ID = :ORDER_ID;
END
END
END!!
CREATE TRIGGER CHECK_NEW_ORDER_ITEMS FOR ORDERS
AFTER INSERT
AS BEGIN
/* When an order is added, we need to eventually
* check that it has items. */
INSERT INTO ORDERS_TO_CHECK (
ORDER_ID
) VALUES (
NEW.ORDER_ID
);
END!!
CREATE TRIGGER CHECK_UPDATED_ORDER_ITEMS FOR ORDERS
AFTER UPDATE
AS BEGIN
/* When an order is updated, we need to eventually
* check that it has items with its new ID. */
IF (NEW.ORDER_ID <> OLD.ORDER_ID) THEN BEGIN
INSERT INTO ORDERS_TO_CHECK (
ORDER_ID
) VALUES (
NEW.ORDER_ID
);
END
END!!
CREATE TRIGGER CHECK_ORDER_UPDATED_ITEMS FOR ORDER_ITEMS
AFTER UPDATE
AS BEGIN
/* When an order item is assigned to another order,
* we need to check that the old order is not left
* without any items. */
IF (NEW.ORDER_ID <> OLD.ORDER_ID) THEN BEGIN
INSERT INTO ORDERS_TO_CHECK (
ORDER_ID
) VALUES (
OLD.ORDER_ID
);
END
END!!
CREATE TRIGGER CHECK_ORDER_DELETED_ITEMS ORDER_ITEMS
AFTER DELETE
AS BEGIN
/* When an order item is deleted, we need to check that
* the order is not left without any items. */
INSERT INTO ORDERS_TO_CHECK (
ORDER_ID
) VALUES (
OLD.ORDER_ID
);
END!!
SET TERM ;!!
OK, we can try these out now. If we add a new order but don't add any items, an exception should be thrown when we invoke the stored procedure to verify these things.
INSERT INTO ORDERS (ORDER_ID, DELIVERED) VALUES (2, 'T');
SELECT * FROM ORDERS_TO_CHECK;
ORDER_ID
============
2
EXECUTE PROCEDURE CHECK_ORDERS;
Statement failed, SQLCODE = -836
exception 1
-Orders require order items to be stored in the database.
Now that we can track what validation we need to perform and we have the logic to do so, the second problem is when do we run the procedure that checks things. The obvious place is right before a transaction commits. This would then abort the transaction with the raised exception if there were any problems. Firebird does not have triggers for a 'before-commit' event, so we could do this from the client side or from the server side if we clearly define entry points to server procedures. This is usually workable for controlled environments; you may want to have a single CHECK_BEFORE_COMMIT procedure to centrally handle any separate constraints you want to enforce. However, this is not always an acceptable solution, for performance or security reasons, and so there is another variation that is interesting. Sometimes you can have a daemon running that will run the checks at scheduled intervals. In this case, you can use the exception-throwing approach, or you can modify the interface of the procedure to actually report the problems, or you could get creative and write a compensating transaction. Read more about FireBird Offline Processing here. |