I have a database of polymorphic structure: a "base" type table and two "derived" types:
CREATE TABLE ContactMethod(
id integer PRIMARY KEY
person_id integer
priority integer
allow_solicitation boolean
FOREIGN KEY(person_id) REFERENCES People(id)
)
CREATE TABLE PhoneNumbers(
contact_method_id integer PRIMARY KEY
phone_number varchar
FOREIGN KEY(contact_method_id) REFERENCES ContactMethod(id)
)
CREATE TABLE EmailAddresses(
contact_method_id integer PRIMARY KEY
email_address varchar
FOREIGN KEY(contact_method_id) REFERENCES ContactMethod(id)
)
I want to prevent orphaned ContactMethod
records from existing, that is, a ContactMethod
record with neither a corresponding PhoneNumber
record nor an EmailAddress
record. I've seen techniques for ensuring exclusivity (preventing a ContactMethod
record with both a related PhoneNumber
and EmailAddress
), but not for preventing orphans.
One idea is a CHECK constraint that executes a custom function that executes queries. However, executing queries via functions in CHECK constraints is a bad idea.
Another idea is a View that will trigger a violation if an orphaned ContactMethod
record is added. The "obvious" way to do this is to put a constraint on the View, but that's not allowed. So it has to be some sort of trick, probably involving an index on the View. Is that really the best (only?) way to enforce no orphans? If so, what is a working example?
Are there other ways? I could get rid of ContactMethod
table and duplicate shared columns on the other two tables, but I don't want to do that. I'm primarily curious about capabilities available in MySQL and SQLite, but a solution in any SQL engine would be helpful.
The simplest solution would be to use single table inheritance. So both the contact methods are optional (that is, nullable) fields in the ContactMethod table, but you add a CHECK constraint to ensure at least one of these has a non-null value.
CREATE TABLE ContactMethod(
id integer PRIMARY KEY
person_id integer
priority integer
allow_solicitation boolean,
phone_number varchar DEFAULT NULL
email_address varchar DEFAULT NULL
FOREIGN KEY(person_id) REFERENCES People(id)
CHECK (COALESCE(phone_number, email_address) IS NOT NULL)
)
Another solution that supports polymorphic associations is to reverse the direction of foreign key. Make ContactMethod have a one nullable foreign key for each type of associated method. Use a CHECK to make sure at least one has a non-null value. This works because you don't allow multiple emails or phones per row in ContactMethod. It does mean if you add a different type of contact (e.g. Signal account), then you'd have to add another foreign key to this table.
CREATE TABLE ContactMethod(
id integer PRIMARY KEY
person_id integer
priority integer
allow_solicitation boolean,
phone_number_id integer DEFAULT NULL
email_address_id integer DEFAULT NULL
FOREIGN KEY(person_id) REFERENCES People(id)
FOREIGN KEY(phone_number_id) REFERENCES PhoneNumbers(id)
FOREIGN KEY(email_address_id) REFERENCES EmailAddresses(id)
CHECK (COALESCE(phone_number_id, email_address_id) IS NOT NULL)
)