Εισαγωγή
Το Polymorphic Association είναι ένα σχήμα όπου ένας πίνακας (π.χ. Comments) συνδέεται με πολλαπλούς πιθανούς parents μέσω δύο πεδίων: target_id και target_type (π.χ. 'Order', 'Product'). Αν και φαίνεται ευέλικτο από πλευράς εφαρμογής (λιγότερες αλλαγές στο schema όταν προστίθεται νέος τύπος), σε σχεσιακές βάσεις όπως το SQL Server αποτελεί Anti‑Pattern επειδή παραβιάζει τo Referential Integrity, αυξάνει την πολυπλοκότητα των queries, περιορίζει τη βελτιστοποίηση του optimizer και καθιστά δύσκολους τους μηχανισμούς cascade.
Polymorphic Association Anti‑Pattern
Παράδειγμα Anti‑Pattern
-- Parent tables
CREATE TABLE dbo.Orders (
order_id int PRIMARY KEY,
order_number nvarchar(50) NOT NULL
);
CREATE TABLE dbo.Products (
product_id int PRIMARY KEY,
product_name nvarchar(200) NOT NULL
);
-- Πολυμορφικός πίνακας σχολίων (ANTI-PATTERN)
CREATE TABLE dbo.Comments (
comment_id int IDENTITY PRIMARY KEY,
target_id int NOT NULL, -- δείχνει είτε σε Orders είτε σε Products
target_type varchar(20) NOT NULL, -- 'Order' ή 'Product'
comment_text nvarchar(1000) NOT NULL,
created_at datetime2(0) NOT NULL DEFAULT sysdatetime()
-- ΠΡΟΣΟΧΗ: Δεν υπάρχει FK εδώ!
);
-- Εισαγωγή δεδομένων (με ορφανό παράδειγμα)
INSERT dbo.Orders(order_id, order_number) VALUES (1, N'ORD-001');
INSERT dbo.Products(product_id, product_name) VALUES (10, N'Keyboard');
INSERT dbo.Comments(target_id, target_type, comment_text)
VALUES
(1, 'Order', N'Πρώτο σχόλιο για την παραγγελία 1'),
(10, 'Product', N'Σχόλιο για το προϊόν 10'),
(999, 'Order', N'Ορφανό σχόλιο (Order 999 δεν υπάρχει)');
Ενδεικτικό query (δύσχρηστο & εύθραυστο)
SELECT
c.comment_id,
c.comment_text,
c.target_type,
COALESCE(o.order_number, p.product_name) AS parent_display
FROM dbo.Comments AS c
LEFT JOIN dbo.Orders AS o ON c.target_type = 'Order' AND c.target_id = o.order_id
LEFT JOIN dbo.Products AS p ON c.target_type = 'Product' AND c.target_id = p.product_id;
Τι προβλήματα δημιουργεί
- Έλλειψη Referential Integrity: Χωρίς FK, προκύπτουν ορφανά. Η ακεραιότητα βασίζεται στην εφαρμογή αντί για τη DB.
- Δυσκολία στη βελτιστοποίηση: Ο optimizer δεν έχει σαφή στατιστικά σχέσεων, τα indexes είναι σύνθετα (π.χ. (target_type, target_id)) και συχνά ανεπαρκή.
- Cascades/Constraints: Δεν μπορείς να ορίσεις ON DELETE CASCADE προς πολλούς parent tables. Triggers/constraints γίνονται περίπλοκα και εύθραυστα.
- Maintainability: Κάθε προσθήκη νέου target_type επιβάλλει αλλαγές σε queries, indexes, triggers, reports, κ.λπ.
- Σύνθετη επιχειρησιακή λογική: Πολλά CASE/IF σε stored procedures, views και ETL.
Γιατί παρόλα αυτά χρησιμοποιείται
- Ευελιξία στο application layer: Ελάχιστες αλλαγές στο schema όταν προστίθεται νέος τύπος οντότητας.
- Γρήγορη αρχική ανάπτυξη: Ενιαίος πίνακας για «σχόλια» ή «συνημμένα».
- ORM υποστήριξη: Κάποια ORMs διευκολύνουν το pattern στο code‑first επίπεδο.
Danger
Παρ’ όλα αυτά, στα transactional συστήματα με απαιτήσεις ακεραιότητας/απόδοσης, το pattern συνήθως πληρώνεται ακριβά μακροπρόθεσμα.
Εναλλακτικές Λύσεις
Εναλλακτική Α: Ξεχωριστά Association Tables (ρητά FKs)
Διατηρείς διαφορετικούς πίνακες συσχέτισης ανά parent table, με καθαρά Foreign Keys.
Εναλλακτική Α: Ξεχωριστά Association Tables
Πλεονεκτήματα: Καθαρά FKs, ON DELETE CASCADE, σαφή στατιστικά, απλά JOINs.
Κόστος: Περισσότεροι πίνακες (επαναλαμβανόμενη δομή).
Ξεχωριστά Association Tables (ρητά FKs)
CREATE TABLE dbo.OrderComments (
comment_id int IDENTITY PRIMARY KEY,
order_id int NOT NULL FOREIGN KEY REFERENCES dbo.Orders(order_id),
comment_text nvarchar(1000) NOT NULL,
created_at datetime2(0) NOT NULL DEFAULT sysdatetime()
);
CREATE TABLE dbo.ProductComments (
comment_id int IDENTITY PRIMARY KEY,
product_id int NOT NULL FOREIGN KEY REFERENCES dbo.Products(product_id),
comment_text nvarchar(1000) NOT NULL,
created_at datetime2(0) NOT NULL DEFAULT sysdatetime()
);
Εναλλακτική Β: Unified Base Entity (Table‑per‑Type με κοινό entity_id)
Όλα τα parent tables αποκτούν κοινή οντότητα (Entities) και μοιράζονται το ίδιο κλειδί.
Εναλλακτική Β: Unified Base Entity (Table‑per‑Type με κοινό entity_id)
Εναλλακτική Β: Unified Base Entity (Table‑per‑Type με κοινό entity_id)
CREATE TABLE dbo.Entities (
entity_id bigint IDENTITY PRIMARY KEY,
entity_type varchar(20) NOT NULL -- 'Order','Product',...
);
CREATE TABLE dbo.Orders (
order_id bigint PRIMARY KEY, -- == entity_id
order_number nvarchar(50) NOT NULL,
CONSTRAINT FK_Orders_Entity FOREIGN KEY (order_id)
REFERENCES dbo.Entities(entity_id)
ON DELETE CASCADE
);
CREATE TABLE dbo.Products (
product_id bigint PRIMARY KEY, -- == entity_id
product_name nvarchar(200) NOT NULL,
CONSTRAINT FK_Products_Entity FOREIGN KEY (product_id)
REFERENCES dbo.Entities(entity_id)
ON DELETE CASCADE
);
CREATE TABLE dbo.Comments (
comment_id bigint IDENTITY PRIMARY KEY,
entity_id bigint NOT NULL
FOREIGN KEY REFERENCES dbo.Entities(entity_id) ON DELETE CASCADE,
comment_text nvarchar(1000) NOT NULL,
created_at datetime2(0) NOT NULL DEFAULT sysdatetime()
);
Πλεονεκτήματα: Ένα FK για τα σχόλια, πλήρη cascades μέσω Entities, εύκολη επέκταση με νέους τύπους.
Κόστος: Απαιτεί ενοποιημένη ταυτότητα και διαδικασία εισαγωγής (πρώτα Entities, μετά το child).
Πίνακας Σύγκρισης
| Κριτήριο |
Polymorphic Association (Anti-Pattern) |
Ξεχωριστά Association Tables |
Unified Entity Table |
| Referential Integrity |
Καμία εγγύηση |
Πλήρης μέσω FKs |
Πλήρης μέσω FKs |
| ON DELETE CASCADE |
Αδύνατο |
Υποστηρίζεται |
Υποστηρίζεται |
| Query Complexity |
Πολύπλοκα JOIN με CASE |
Απλά JOIN |
Απλά JOIN |
| Indexing & Statistics |
Δύσκολα, χωρίς FK στατιστικά |
Καθαρά στατιστικά ανά σχέση |
Καθαρά στατιστικά |
| Maintainability |
Δύσκολο, triggers για ακεραιότητα |
Εύκολο, απλή δομή |
Εύκολο, αλλά πιο σύνθετη εισαγωγή |
| Επεκτασιμότητα (νέοι τύποι) |
Εύκολη (προσθήκη νέου target_type) |
Νέος πίνακας για κάθε τύπο |
Εύκολη (νέος child table) |
| ORM Compatibility |
Υποστηρίζεται |
Υποστηρίζεται |
Υποστηρίζεται |
| Απόδοση σε μεγάλα datasets |
Χαμηλή (πολλά CASE & JOIN) |
Υψηλή |
Υψηλή |
Συμπεράσματα & Best Practices
- Απέφυγε το Polymorphic Association για core transactional δεδομένα. Η βραχυπρόθεσμη ευελιξία κοστίζει ακριβά σε ακεραιότητα, απόδοση και συντηρησιμότητα.
- Προτίμησε:
Ξεχωριστά association tables όταν οι τύποι είναι λίγοι/σταθεροί και θέλεις απλότητα.
Unified base entity όταν χρειάζεσαι επεκτασιμότητα με κοινό identity και πλήρη cascades.
- Χτίσε RBAC (roles), FKs, constraints και καθαρά indexes. Μην βασίζεις την ορθότητα σε caching ή triggers.
- Σχεδίασε migrations με views/dual‑write/backfill/cutover για zero‑downtime μετάβαση από polymorphic σε καθαρές σχέσεις