On the value of foreign key constraints

Here is something I would like to demo regarding foreign key constraints.

This regards what type of errors foreign key constraints can (and can’t!) save you from.

Occasionally you hear developers say how important foreign key constraints are and how foreign key constraints saved them from some bug. That might all seem well and good but when you think it through if a foreign key constraint saved you from a bug it also might very well just have been luck that saved you and the thing that went wrong might have gone wrong another way and have gone undetected as database corruption and potential accidental data disclosure to an incorrect entity.

To set the scene let’s use some schema/data from what we were talking about earlier:

create table t_skinny_customer (
  customer_id bigint not null primary key,
  customer_name varchar(255) not null,
  customer_record_created_on timestamp default current_timestamp,
  customer_record_updated_on timestamp default current_timestamp on update current_timestamp
);

create table t_skinny_customer_phone (
  customer_phone_id bigint not null primary key,
  customer_id bigint not null references t_skinny_customer( customer_id ) on delete cascade,
  phone_number varchar(255) not null,
  phone_type enum( 'phone', 'tollfree', 'mobile' ) not null default 'phone',
  phone_type_order tinyint unsigned not null default 0,
  phone_record_created_on timestamp default current_timestamp,
  phone_record_updated_on timestamp default current_timestamp on update current_timestamp
);

insert into t_skinny_customer (
  customer_id,
  customer_name
)
values (
  1,
  'John Doe'
), (
  2,
  'Jane Doe'
);

insert into t_skinny_customer_phone (
  customer_phone_id,
  customer_id,
  phone_number,
  phone_type,
  phone_type_order
)
values (
  11,
  1,
  '123-456-7890',
  'phone',
  1
), (
  21,
  2,
  '123-456-7894',
  'phone',
  1
);

Then let’s run this code:

    $stmt = $pdo->prepare("
      update
        t_skinny_customer_phone
      set
        customer_id = :customer_id
      where
        customer_phone_id = :phone_id
    ");

    try {

      $stmt->execute([
        'customer_id' => 3,
        'phone_id' => 11,
      ]);

      assert( false );

    }
    catch ( PDOException $ex ) {

      // 2024-02-13 jj5 - foreign key constraint saved the day!

      assert(
        0 === strpos(
          $ex->getMessage(),
          'SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails'
        )
      );

    }

    $stmt->execute([
      'customer_id' => 2,
      'phone_id' => 11,
    ]);

    // 2024-02-13 jj5 - ... whoops! what we've done here is switch
    // one of John Doe's phone numbers to be one of Jane Doe's
    // phone numbers. The foreign key constraint is of no value in
    // detecting this type of data corruption.

Now I’m not saying that you shouldn’t have foreign key constraints. I’m just pointing out that if you do have foreign key constraints and you’re relying on them for “referential integrity” your door may be open for all manner of bugs (and exploits).

That your foreign key points to an actual record doesn’t necessarily imply that your foreign key points to a correct record.

Database design and normalization

I would like to compare and contrast two options for the design of customer contact information for a relational database.

This is the first option, a “fat” customer table.

create table t_fat_customer (
  customer_id bigint not null primary key,
  customer_name varchar(255) not null,
  customer_phone_1 varchar(255) not null default '',
  customer_phone_2 varchar(255) not null default '',
  customer_tollfree_1 varchar(255) not null default '',
  customer_tollfree_2 varchar(255) not null default '',
  customer_mobile_1 varchar(255) not null default '',
  customer_mobile_2 varchar(255) not null default '',
  customer_record_created_on timestamp default current_timestamp,
  customer_record_updated_on timestamp default current_timestamp on update current_timestamp
);

insert into t_fat_customer (
  customer_id,
  customer_name,
  customer_phone_1,
  customer_phone_2,
  customer_tollfree_1,
  customer_tollfree_2,
  customer_mobile_1,
  customer_mobile_2
)
values (
  1,
  'John Doe',
  '123-456-7890',
  '123-456-7891',
  '800-123-4567',
  '',
  '123-456-7892',
  ''
), (
  2,
  'Jane Doe',
  '123-456-7894',
  '123-456-7895',
  '800-123-4569',
  '',
  '123-456-7896',
  ''
);

This is the second option, a “skinny” and “properly normalized” option. It has lots of skinny tables, which some will claim are the hallmarks of “good design”.

create table t_skinny_customer (
  customer_id bigint not null primary key,
  customer_name varchar(255) not null,
  customer_record_created_on timestamp default current_timestamp,
  customer_record_updated_on timestamp default current_timestamp on update current_timestamp
);

create table t_skinny_customer_phone (
  customer_phone_id bigint not null primary key,
  customer_id bigint not null references t_skinny_customer( customer_id ) on delete cascade,
  phone_number varchar(255) not null,
  phone_type enum( 'phone', 'tollfree', 'mobile' ) not null default 'phone',
  phone_type_order tinyint unsigned not null default 0,
  phone_record_created_on timestamp default current_timestamp,
  phone_record_updated_on timestamp default current_timestamp on update current_timestamp
);

insert into t_skinny_customer (
  customer_id,
  customer_name
)
values (
  1,
  'John Doe'
), (
  2,
  'Jane Doe'
);

insert into t_skinny_customer_phone (
  customer_phone_id,
  customer_id,
  phone_number,
  phone_type,
  phone_type_order
)
values (
  11,
  1,
  '123-456-7890',
  'phone',
  1
), (
  12,
  1,
  '123-456-7891',
  'phone',
  2
), (
  13,
  1,
  '800-123-4567',
  'tollfree',
  1
), (
  14,
  1,
  '123-456-7892',
  'mobile',
  1
), (
  21,
  2,
  '123-456-7894',
  'phone',
  1
), (
  22,
  2,
  '123-456-7895',
  'phone',
  2
), (
  23,
  2,
  '800-123-4569',
  'tollfree',
  1
), (
  24,
  2,
  '123-456-7896',
  'mobile',
  1
);

Here are some stats from the design options:

Fat option Skinny option
Lines of code: 42 85
Tables: 1 2
Columns: 10 11
Records: 2 10

The first option is simpler than the second option by every measure.

I know, you want to create a more general solution. I know, they say a hallmark of good database design and “proper normalization” is lots of skinny tables. I remind you: the first option is simpler than the second option by every measure.

My advice is don’t rush toward general solutions in an effort to solve problems you don’t have.

The UI isn’t coupled to the database design in any way. You could implement any type of UI on top of either database design alternative, such as:

Primary Secondary Operations
Phone:
Tollfree:
Mobile:

Or:

Phone Type Phone Number Operations

(My opinion is that the first UI option above is superior to the second UI option, it’s also exceedingly easy to implement with the “fat” customer table.)

Here are some implementation exercises to compare the difficulty of working with the database options:

  • given a customer ID get the primary tollfree number or the primary phone number if there’s no tollfree number on record
  • for each customer, fix the phone number records so that the secondary contact option is promoted to the primary contact option if there isn’t already a primary option on record, for each phone type (phone, tollfree, mobile).
  • implement user interface option A above
  • implement user interface option B above
  • create a view to present the “fat” customer contact options as a table like in the “skinny” t_skinny_customer_phone table
  • create a view to present the “skinny” customer contact options as a table like in the “fat” t_fat_customer table
  • enforce a business rule which says that not more than two contact options for each phone type should be stored in the database for each customer
  • check for duplicate contact phone numbers and remove them when found
  • put the customer contact information into a spreadsheet and email to marketing by COB

New website: Take Two

Hey Craig. I’ve made some changes (hopefully improvements!) to the website:

Only the home page is working at the moment. I will get the other pages working soon, hopefully today.

Please do let me know (in no uncertain terms!) what you think of the new design. I’ve gone “mobile first”, so it should look respectable on your smart phone.

Passing myself off as respectable

There’s been a little bit of spring cleaning around here today!

I’m trying to get my house in order so I can pass myself off as respectable when I start promoting my new show In The Lab With Jay Jay in earnest.

To that end I have reviewed the look and feel of my wiki. It no longer has that black, green, and orange monospaced font vibe going on, but now uses the relatively unoffensive default theme.

It’s not using the latest Vector theme, but rather the legacy one. This was deliberate because I have a lot of big tables in my wiki (such as on my computers and shopping pages) and the legacy theme plays nicer with those.

I’ve also been tweaking the website for my show, including an updated about page.

The blog, the wiki, and the show also now all use my new favicon which is my take on the Hacker Emblem:

John's new icon, based on the Hacker Emblem

Context object versus global variables

I’m reading A Philosophy of Software Design by John Ousterhout and he says:

The context object unifies the handling of all system-global information and eliminates the need for pass-through variables. If a new variable needs to be added, it can be added to the context object; no existing code is affected except for the constructor and destructor for the context. The context makes it easy to identify and manage the global state of the system, since it is all stored in one place. The context is also convenient for testing: test code can change the global configuration of the application by modifying fields in the context. It would be much more difficult to implement such changes if the system used pass-through variables.

Contexts are far from an ideal solution. The variables stored in a context have most of the disadvantages of global variables; for example, it may not be obvious why a particular variable is present, or where it is used. Without discipline, a context can turn into a huge grab-bag of data that creates nonobvious dependencies throughout the system. Contexts may also create thread-safety issues; the best way to avoid problems is for variables in a context to be immutable. Unfortunately, I haven’t found a better solution than contexts.

Okay, so I’m just gonna step way out of line over here and suggest something heretical… but shouldn’t you just use global variables? You only introduced the context object so you could tweak it in unit tests, and you could just change your tests so that each one ran in a new process. Just sayin’.

…I suppose for the sake of completeness I should add a little more from Ousterhout which he said prior to the above:

Another approach is to store the information in a global variable, as in Figure 7.2(c). This avoids the need to pass the information from method to method, but global variables almost always create other problems. For example, global variables make it impossible to create two independent instances of the same system in the same process, since accesses to the global variables will conflict. It may seem unlikely that you would need multiple instances in production, but they are often useful in testing.

…so he is bending over backward to support multiple tests in one process, but he could just run each test in its own process and his problem evaporates.

6 Horribly Common PCB Design Mistakes and AppCAD

Today I enjoyed watching 6 Horribly Common PCB Design Mistakes and the follow-up video Design Mistakes You Must Avoid on Your New Electronic Product.

In the first video I learned about AppCAD Design Assistant which is a free tool which boasts a broad bunch of features (and, particularly, it can help you design an antenna on your PCB):

  • S-Parameter Analysis and Plotting
  • Active Circuit Bias Design
  • Cascade Noise and IP3 Analysis
  • Transmission Line Analysis
  • Signals and Systems
  • Complex Math Engineering Calculator

I haven’t used AppCAD yet, but I will check it out soon.