Entitäten und deren Identifier

Gedanken

Identifier helfen dabei, manche Entitäten – eindeutig – zu benennen. Diesmal starte ich unten im Code-Model und mit einem technischen Detail: Wie gestalte ich die Tabellen am besten?

CREATE TABLE schema.table
(
    id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY

meistens findet man sowas vor. Die erste Frage, welche sich stellt: Sollte man diese id nach außen hin zur Verfügung stellen? E.g. in einer API wie /persons/{id}? Meine Meinung dazu: nein. Einige Gründe:

  • Refactoring von einer Tabelle zu Table inheritance: Hier haben die Tabellen eigene Id Spalten.
  • Wechseln von einem einfachen Primary-Key zu einem Composite-Primary-Key (PK über mehrere Spalten) - braucht man oft bei Constraints
  • Normalization -/Denormalization (z.B. Order und OrderItem Table wird zu CustomerOrders - man müsste hier den PK der Order als Feld von CustomerOrders machen, welches aber alleine inkrementiert)
  • Importieren von Daten aus u.a. mehreren Quellen, die bereits einen Identifier haben oder Database-Merge

Aber es gibt auch technische Gründe, wie folgende Artikel zeigen:

Daher die Empfehlung:

CREATE TABLE schema.table
(
    id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    external_id uuid NOT NULL UNIQUE

UUID – aber welche?

Einer der kompliziertesten OpenAPI Specs die ich je gesehen haben, sind jene aus dem TM-Forum. Arbeitet man mit diesen, kommt man oft in folgende Situation:

Entwickler A: Kannst du dir bitte mal Id e3c5c1df-f871-4c24-b7f0-6f882f5f0a01 anschauen?

Entwickler B: Was ist das für eine Id?

Entwickler A: Ich glaube es ist irgendein Preis – aber genau weiß ich es auch nicht

Wie könnte man hier entgegenwirken? Eine Möglichkeit ist natürlich Uniform Resource Name (URN).

urn:ISBN:3-8273-7019-1 lässt niemanden mehr rätseln, was 3-8273-7019-1 ist.

Andere Beispiele

Azure

Eine Id einer Ressource schaut wie folgt aus:

/subscriptions/dd26faa9-ad5a-45fd-aa12-685cb5285f0f/resourceGroups/rg-website-prod-001/providers/Microsoft.Web/staticSites/vodepat

GCP

Schema-less Version von https://datatracker.ietf.org/doc/html/rfc3986

//library.googleapis.com/shelves/shelf1/books/book2

Ein URI-Pfad (path-noscheme) ohne das führende “/”: https://datatracker.ietf.org/doc/html/rfc3986#appendix-A

shelves/shelf1/books/book2

AWS

Hier wurde ein eigener Format definiert: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html

arn:partition:service:region:account-id:resource-type/resource-id

Examples:

arn:aws:iam::123456789012:user/johndoe arn:aws:ec2:us-east-1:123456789012:vpc/vpc-0e9801d129EXAMPLE

Oracle

https://docs.oracle.com/en-us/iaas/Content/General/Concepts/identifiers.htm

ocid1.<RESOURCE TYPE>.<REALM>.[REGION][.FUTURE USE].<UNIQUE ID>

ocid1: The literal string indicating the version of the OCID.

IBM

https://cloud.ibm.com/docs/account?topic=account-crn

Cloud Resource Name (CRN):

crn:version:cname:ctype:service-name:location:scope:service-instance:resource-type:resource

Identifier korrekt im Code

Auch im Code gibt es oft die Aufgabe

Entwickler A: Such mir bitte alle Stellen raus, wo wir mit der Product Id hantieren – wir müssen den Typ ändern und einige Well-Known-Ids unterbinden

Entwickler B: Das kann dauern … die Variable “productId” ist oft nicht immer das Gleiche … Manchmal heißt es auch nur Id und manchmal wurde die UUID zu einem String gemapped …

Auf den Variablen Namen “productId” kann man sich kaum verlassen. Daher verwende ich immer gerne ValueObjects. Gott sei Dank kennt C# inzwischen Records, da diese automatisch immutable sind.

ProductId mit dem passenden Namespace findet man daher sehr leicht.

using System;
using System.Text.RegularExpressions;
 
record ProductId
{
    public string URN { get; }
 
    private static readonly string URNPattern = @"^urn:ISBN:\d{1,5}-\d{1,7}-\d{1,7}-\d{1}$";
 
    public ProductId(string urn)
    {
        if (!IsValidURN(urn))
        {
            throw new ArgumentException("Invalid URN: ISBN format is not valid.", nameof(urn));
        }
 
        URN = urn;
    }
 
    private static bool IsValidURN(string urn)
    {
        return Regex.IsMatch(urn, URNPattern);
    }
}
 
class Program
{
    static void Main()
    {
        try
        {
            ProductId book = new ProductId("urn:ISBN:12345-6789101-2345678-9");
            Console.WriteLine($"URN: {book.URN}");
        }
        catch (ArgumentException ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

URN in der Datenbank

Auch in der Datenbank kann man mit URN hantieren. Immer im Hinterkopf muss man natürlich Indezeses behalten. Dies können natürlich computed sein - wie folgendes Beispiel zeigt:

CREATE INDEX test1_lower_col1_idx ON test1 (lower(col1));

Aber das mit und ohne URN Supported geht, funktioniert nur mit Operatoren. Und selbst da muss man aufpassen, dass er den Operator nimmt - sonst kommt es zu einem Textvergleich und urn:ISBN:3-8273-7019-1 ist natürlich nicht 3-8273-7019-1.

DROP SCHEMA IF EXISTS playground CASCADE;
CREATE SCHEMA IF NOT EXISTS playground;

CREATE DOMAIN playground.isbn AS text CONSTRAINT valid_isbn CHECK (VALUE ~ '^\d{1,5}-\d{1,7}-\d{1,7}-\d{1}$');
CREATE DOMAIN playground.urn_isbn AS text CONSTRAINT valid_urn_isbn CHECK (VALUE ~ '^urn:ISBN:\d{1,5}-\d{1,7}-\d{1,7}-\d{1}$');

CREATE OR REPLACE FUNCTION playground.extract_isbn(isbn, urn_isbn)
  RETURNS BOOL
AS $$
BEGIN
    RETURN $1 = substring($2 from length('urn:ISBN:') + 1);
END;
$$
LANGUAGE plpgsql STABLE;

CREATE OPERATOR playground.= (
    LEFTARG = isbn,
    RIGHTARG = urn_isbn,
    PROCEDURE = playground.extract_isbn
);

CREATE TABLE playground.products
(
    id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    external_id isbn NOT NULL UNIQUE
);

INSERT INTO playground.products (external_id) VALUES('12345-6789101-2345678-9');

SELECT * FROM playground.products WHERE external_id = 'urn:ISBN:12345-6789101-2345678-9'::urn_isbn

Domain-Driven-Design und Identifier

Auch in dem DDD wird dem Identifier gorße Bedeutung zugeschrieben. Folgende Seiten zeigen interessante Ideen:

  • https://ddd-practitioners.com/home/glossary/entity-identity/
    • Hier wird nochmal mit Nachdruck drauf hingewiesen, dass der Identifier eindeutig und stabil sehen sollte. Die menschliche Lesbarkeit kann man mit URN nochmal erhöhen und trptzdem die Vorteile einer UUID nutzen.
  • In DDD ist der Identifier auch ausschlaggebend für ein Entity - im Kontrast zu einem ValueObject
  • DDD empfiehlt auch “natural identifiers”. Beispiel ist für mich WhatsApp mit einer Telefonnummer oder Microsoft Online Dienste mit einer Email:

In some cases, the uniqueness of the ID must apply beyond the computer system’s boundaries. For example, if medical records are being exchanged between two hospitals that have separate computer systems, ideally each system will use the same patient ID, but this is difficult if they generate their own symbol. Such systems often use an identifier issued by some other institution, typically a government agency. In the United States, the Social Security number is often used by hospitals as an identifier for a person. Such methods are not foolproof. Not everyone has a Social Security number (children and nonresidents of the United States, especially), and many people object to its use, for privacy reasons.

In less formal situations (say, video rental), telephone numbers are used as identifiers. But a telephone can be shared. The number can change. An old number can even be reassigned to a different person.

For these reasons, specially assigned identifiers are often used (such as frequent flier numbers), and other attributes, such as phone numbers and Social Security numbers, are used to match and verify. In any case, when the application requires an external ID, the users of the system become responsible for supplying IDs that are unique, and the system must give them adequate tools to handle exceptions that arise.

Quelle: Evans, E. (2004). Domain-driven design: tackling complexity in the heart of software. Addison-Wesley Professional.

Slug

Im Web gibt es ähnliche Konzepte.

Some systems define a slug as the part of a URL that identifies a page in human-readable keywords

Ein Beispiel wäre:

https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-processing-an-unsorted-array

Fazit

Identifier sind extrem wichtig und sollten einen festen Bestandteil im Design haben.