Inhaltsverzeichnis
2.2 ENIVERSCAFM Asset-Modell
Stand: 2026-03-05
Übergeordnet: CAFM-Architektur — Gesamtübersicht Verwandt: 2.1 Systemebenen | 2.4 Migration
Kernkonzept: asset.item
asset.item ist die universelle Tabelle für Technische Plätze und Equipment.
Beide Konzepte werden in derselben Tabelle geführt — der Unterschied liegt im
status_code und der Verknüpfung über asset.item_installation.
Tabellenstruktur
CREATE TABLE asset.item ( id INT NOT NULL IDENTITY PRIMARY KEY, legacy_id VARCHAR(50) NULL, -- Original-PK aus Quellsystem legacy_source VARCHAR(20) NULL, -- 'AMED','ENIVERS','TECDB',... type_code VARCHAR(20) NOT NULL, -- FK → asset.item_type.code status_code VARCHAR(20) NOT NULL, -- 'planned','active','decommissioned' item_number VARCHAR(50) NULL, -- Anlagenkennzeichen / EMR-Stelle name NVARCHAR(200) NOT NULL, description NVARCHAR(MAX) NULL, branch_id INT NULL, -- FK → org.branch location_id INT NULL, -- FK → org.location parent_id INT NULL, -- FK → asset.item.id (TP-Hierarchie) properties NVARCHAR(MAX) NULL, -- JSON: type-spezifische Felder is_active BIT NOT NULL DEFAULT 1, created_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(), updated_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(), created_by INT NULL, updated_by INT NULL )
Befüllungsstand (Ist-Stand 2026-03-05)
| type_code | Anzahl Datensätze | Herkunft |
|---|---|---|
| device | 11.652 | AMED APPMASTER (erwAppNr) |
| motor | 8.461 | AMED MOTORMASTER |
| instrument | — | Geplant (EZAMASTER-Migration) |
| lighting | — | Geplant (tblLocationKV-Migration) |
| hvac | — | Geplant |
| safety | — | Geplant (AVI_Equip-Migration) |
| electrical | — | Geplant (BESTAND/LD-Migration) |
| piping | — | Geplant (TecDB-Migration) |
| structure | — | Geplant (Standortstruktur) |
Metadaten-getriebenes Schema: asset.property_schema
asset.property_schema beschreibt alle Felder eines type_code.
Formulare, Validierungen und Views werden daraus generiert — ohne hardcodierte Logik.
Tabellenstruktur (Auszug)
CREATE TABLE asset.property_schema ( id INT NOT NULL IDENTITY PRIMARY KEY, type_code VARCHAR(20) NOT NULL, property_name VARCHAR(100) NOT NULL, display_name NVARCHAR(200) NOT NULL, display_group NVARCHAR(100) NULL, sql_type VARCHAR(20) NOT NULL, -- 'nvarchar','int','decimal','bit',... sql_length INT NULL, sql_precision INT NULL, sql_scale INT NULL, is_lookup BIT NOT NULL DEFAULT 0, lookup_schema VARCHAR(50) NULL, lookup_table VARCHAR(100) NULL, lookup_key_col VARCHAR(100) NULL, lookup_disp_col VARCHAR(100) NULL, is_required BIT NOT NULL DEFAULT 0, is_readonly BIT NOT NULL DEFAULT 0, is_unique BIT NOT NULL DEFAULT 0, default_value NVARCHAR(200) NULL, min_value NVARCHAR(50) NULL, max_value NVARCHAR(50) NULL, regex_pattern NVARCHAR(500) NULL, allowed_values NVARCHAR(MAX) NULL, -- JSON Array unit NVARCHAR(50) NULL, ui_widget VARCHAR(50) NULL, -- 'text','select','checkbox','date',... is_searchable BIT NOT NULL DEFAULT 1, is_filterable BIT NOT NULL DEFAULT 1, is_sortable BIT NOT NULL DEFAULT 1, is_exportable BIT NOT NULL DEFAULT 1, is_visible BIT NOT NULL DEFAULT 1, sort_order INT NOT NULL DEFAULT 0, legacy_column VARCHAR(100) NULL, -- Quellspalte im Altsystem is_active BIT NOT NULL DEFAULT 1 )
Aktueller Stand: 90 Einträge (device-Typ vollständig; motor-Typ vollständig).
Automatische View-Generierung: trg_regen_type_views
Ein DDL-Trigger auf asset.property_schema generiert bei jeder Schema-Änderung
automatisch eine typenspezifische View (z.B. asset.v_item_device).
Vorteil gegenüber dynamischem SQL zur Laufzeit
| Ansatz | Plan Cache | Trigger-Zeitpunkt | Aufwand |
|---|---|---|---|
| Dynamisches SQL (Laufzeit) | Nein | Jeder SELECT | Kein Plan-Cache möglich |
| trg_regen_type_views | Ja | Schema-Änderung | einmalig pro Änderung |
Trigger-Logik (vereinfacht)
CREATE OR ALTER TRIGGER asset.trg_regen_type_views ON asset.property_schema AFTER INSERT, UPDATE, DELETE AS BEGIN SET NOCOUNT ON -- 1. Betroffene type_codes sammeln DECLARE @types TABLE (type_code VARCHAR(20)) INSERT INTO @types SELECT DISTINCT type_code FROM inserted UNION SELECT DISTINCT type_code FROM deleted -- 2. Pro type_code: CREATE OR ALTER VIEW generieren DECLARE @TYPE VARCHAR(20), @SQL NVARCHAR(MAX), @cols NVARCHAR(MAX) DECLARE cur CURSOR LOCAL FAST_FORWARD FOR SELECT type_code FROM @types OPEN cur; FETCH NEXT FROM cur INTO @TYPE WHILE @@FETCH_STATUS = 0 BEGIN SET @cols = N'' SELECT @cols += N', ' + CASE ps.sql_type WHEN 'int' THEN 'CAST(JSON_VALUE(i.properties,''$.' + ps.property_name + ''') AS int)' WHEN 'float' THEN 'CAST(JSON_VALUE(i.properties,''$.' + ps.property_name + ''') AS float)' WHEN 'decimal' THEN 'CAST(JSON_VALUE(i.properties,''$.' + ps.property_name + ''') AS decimal(' + CAST(ps.sql_precision AS VARCHAR) + ',' + CAST(ps.sql_scale AS VARCHAR) + '))' WHEN 'bit' THEN 'CAST(JSON_VALUE(i.properties,''$.' + ps.property_name + ''') AS bit)' ELSE 'JSON_VALUE(i.properties,''$.' + ps.property_name + ''')' END + ' AS [' + ps.property_name + ']' FROM asset.property_schema ps WHERE ps.type_code = @TYPE AND ps.is_active = 1 ORDER BY ps.sort_order SET @SQL = N'CREATE OR ALTER VIEW asset.v_item_' + @TYPE + N' AS SELECT i.id, i.item_number, i.name, i.description, i.type_code, i.status_code, i.branch_id, i.location_id, i.parent_id, i.legacy_id, i.legacy_source, i.is_active, i.created_at, i.updated_at' + @cols + N' FROM asset.item i WHERE i.type_code = ''' + @TYPE + N'''' EXEC sp_executesql @SQL FETCH NEXT FROM cur INTO @TYPE END CLOSE cur; DEALLOCATE cur END
Der Trigger feuert nur bei Schema-Änderungen (kein Laufzeit-Overhead).
Generierte Views wie asset.v_item_device erhalten einen stabilen Query Plan.
Technischer Platz vs. Equipment: asset.item_installation
Problem
asset.item.parent_id darf nur strukturelle Hierarchie ausdrücken (TP in TP).
Die Frage „welches Equipment ist aktuell an diesem TP verbaut?“ braucht eine separate Relation.
Lösung
CREATE TABLE asset.item_installation ( id INT NOT NULL IDENTITY PRIMARY KEY, fl_item_id INT NOT NULL, -- FK → asset.item (Technischer Platz) eq_item_id INT NOT NULL, -- FK → asset.item (Equipment) installed_from DATE NOT NULL, installed_until DATE NULL, -- NULL = aktuell verbaut removed_reason NVARCHAR(500) NULL, created_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(), created_by INT NULL, CONSTRAINT UQ_installation_active UNIQUE (fl_item_id, installed_until) -- nur ein aktives Equipment pro TP )
TP ↔ Equipment Lebenszyklus
TP (asset.item, status='planned')
│
├── Equipment einbauen
│ → item_installation (installed_from=heute, installed_until=NULL)
│ → Equipment: status='active'
│ → TP: status='active'
│
├── Equipment tauschen
│ → item_installation.installed_until = heute (altes EQ)
│ → neues item_installation für neues EQ
│
└── TP außer Betrieb
→ TP: status='decommissioned'
→ offene item_installation wird geschlossen
Abfrage: aktuell verbautes Equipment
SELECT tp.item_number AS tp_nummer, tp.name AS tp_name, eq.item_number AS eq_nummer, eq.name AS eq_name, ii.installed_from FROM asset.item tp JOIN asset.item_installation ii ON ii.fl_item_id = tp.id AND ii.installed_until IS NULL JOIN asset.item eq ON eq.id = ii.eq_item_id WHERE tp.type_code IN ('device','motor') AND tp.is_active = 1
Elektrische Verknüpfung: asset.electrical_connection
Verknüpft Schaltschrankabgang (kv_number, kv_code) mit einem TP (device_id) und
einem Motor (motor_id).
CREATE TABLE asset.electrical_connection ( id INT NOT NULL IDENTITY PRIMARY KEY, outlet_id INT NULL, -- FK → asset.item (Schaltschrankabgang) device_id INT NULL, -- FK → asset.item (Gerät / TP) motor_id INT NULL, -- FK → asset.item (Motor / Equipment) kv_number NVARCHAR(50) NULL, -- Kraftverteiler-Nummer (aus LD) kv_code NVARCHAR(50) NULL, -- EMR-Stelle (aus LD BESTAND) cabinet NVARCHAR(100) NULL, switchhouse NVARCHAR(100) NULL, properties NVARCHAR(MAX) NULL -- JSON: weitere Felder )
Diese Tabelle löst die Disziplin-Grenze zwischen Mechanik (device/motor) und Elektrik (kv_code): Beide Perspektiven auf dasselbe physische Objekt sind in einem Record sichtbar.
Bausteine: Wiederverwendbare Eigenschaftsgruppen
Ein Baustein ist eine benannte Gruppe von Unter-Eigenschaften, die in mehreren type_code-Schemata
wiederverwendet werden. Beispiel: standort enthält immer Anlage, Betrieb, Gebäude, Stock, Zone —
unabhängig davon, ob das Item ein device, motor oder piping ist.
Erweiterung: asset.property_schema mit parent_code
-- Erweiterung der property_schema-Tabelle: ALTER TABLE asset.property_schema ADD parent_code VARCHAR(100) NULL -- NULL = einfache Eigenschaft -- non-NULL = Unter-Eigenschaft eines Bausteins -- REFERENCES asset.property_schema(property_name + type_code)
Ein Baustein hat sql_type = 'COMPOSITE' und parent_code = NULL.
Seine Unter-Eigenschaften haben parent_code = <baustein_property_name>.
Beispiel: Baustein "standort"
-- Baustein-Definition (sql_type='COMPOSITE') INSERT INTO asset.property_schema (type_code, property_name, display_name, sql_type, parent_code, sort_order) VALUES ('*', 'standort', 'Standort', 'COMPOSITE', NULL, 10); -- Unter-Eigenschaften INSERT INTO asset.property_schema (type_code, property_name, display_name, sql_type, parent_code, sort_order) VALUES ('*', 'standort.anlage', 'Anlage', 'FK_LIST', 'standort', 1), -- list_type='ANLAGE' ('*', 'standort.betrieb', 'Betrieb', 'FK_LIST', 'standort', 2), -- list_type='BETRIEB' ('*', 'standort.gebaeude', 'Gebäude', 'nvarchar','standort', 3), ('*', 'standort.stock', 'Stock / Etage', 'nvarchar','standort', 4), ('*', 'standort.zone', 'Zone', 'nvarchar','standort', 5);
Der Wert eines Bausteins wird als verschachteltes JSON-Objekt in asset.item.properties gespeichert:
{
"standort": {
"anlage": "LDB2",
"betrieb": "2",
"gebaeude": "A",
"stock": "EG",
"zone": "01"
},
"nennleistung": 15.5,
"schutzart": "IP54"
}
Trigger-Erweiterung für COMPOSITE
Der trg_regen_type_views-Trigger muss für sql_type = 'COMPOSITE' erweitert werden:
-- Für COMPOSITE: JSON_QUERY statt JSON_VALUE (gibt Objekt zurück) WHEN 'COMPOSITE' THEN 'JSON_QUERY(i.properties, ''$.' + ps.property_name + ''')' -- Für Unter-Eigenschaften (parent_code IS NOT NULL): gepunkteter Pfad ELSE 'JSON_VALUE(i.properties, ''$.' + ps.parent_code + '.' + ps.property_name_short + ''')'
Weitere Bausteine (geplant)
| Baustein | Unter-Felder | Verwendung |
|---|---|---|
standort | anlage, betrieb, gebaeude, stock, zone | Alle item-Typen |
abmessung | laenge_mm, breite_mm, hoehe_mm, gewicht_kg | device, motor, piping |
elektrisch | spannung_v, strom_a, leistung_kw, frequenz_hz | motor, electrical, safety |
druckmedium | medium, druck_bar, temperatur_c, nennweite_dn | piping (aus TecDB Medien/Faktoren) |
pruefzyklus | iv_code, bz_code, gp_code, pv_code | Link zu PIMS inspection.rule |
Einheitliche Listen: shared.list_item
Alle Auswahllisten (Combo-Boxen, Enums, Lookup-Tabellen) werden in einer einzigen Tabelle geführt. Inspiriert vom WIS-Konzept (RuleKey/Rules), aber allgemeiner anwendbar.
Tabellenstruktur
CREATE TABLE shared.list_item ( id INT NOT NULL IDENTITY PRIMARY KEY, parent_id INT NULL REFERENCES shared.list_item(id), -- für hierarchische Listen list_type VARCHAR(50) NOT NULL, -- z.B. 'ANLAGE', 'ARBEIT_ART', 'SIL_KLASSE' code VARCHAR(20) NOT NULL, -- Kurzschlüssel: 'R', 'LDB2', 'SIL2' label NVARCHAR(200) NOT NULL, -- Anzeige-Text: 'Reparatur', 'LDB2 - Werk 2' value_type VARCHAR(10) NOT NULL DEFAULT 'VARCHAR', -- INT, DATE, NUMERIC, DECIMAL, VARCHAR value_data NVARCHAR(500) NULL, -- gespeicherter Wert (als Text, via value_type konvertieren) sort_order SMALLINT NOT NULL DEFAULT 0, is_active BIT NOT NULL DEFAULT 1, description NVARCHAR(MAX) NULL, CONSTRAINT UQ_list_item_type_code UNIQUE (list_type, code) )
value_type gibt an, in welchen Datentyp value_data beim Lesen konvertiert wird.
Das code-Feld ist der maschinelle Schlüssel (für Programm-Logik);
label ist der angezeigte Text (kann sprachabhängig erweitert werden).
Beispiel-Seed: Arbeitstypen (aus AMED Arbeitsart + ENIVERS ARBEIT.ART)
INSERT INTO shared.list_item (list_type, code, label, sort_order) VALUES ('ARBEIT_ART', 'R', 'Reparatur', 1), ('ARBEIT_ART', 'W', 'Wartung', 2), ('ARBEIT_ART', 'S', 'Störungsbeseitigung', 3), ('ARBEIT_ART', 'U', 'Wiederkehrende Überprüfung', 4), ('ARBEIT_ART', 'B', 'Betriebliche Anforderung', 5), ('ARBEIT_ART', 'P', 'Projektauftrag (Rep./Investition)',6), ('ARBEIT_ART', 'A', 'Wiederkehrende Arbeiten', 7), ('ARBEIT_ART', 'N', 'Normale Arbeiten', 8), ('ARBEIT_ART', 'V', 'Verbesserungsvorschlag', 9), ('ARBEIT_ART', 'K', 'Anlagenänderungen', 10);
Hierarchische Listen (parent_id)
Für Bäume wie Anlage → Betrieb → Betriebseinheit:
-- Ebene 1: Anlage INSERT INTO shared.list_item (list_type, code, label) VALUES ('ANLAGE', 'LDB1', 'LDB1 - Werk 1'); INSERT INTO shared.list_item (list_type, code, label) VALUES ('ANLAGE', 'LDB2', 'LDB2 - Werk 2'); -- Ebene 2: Betrieb (parent_id → LDB2) INSERT INTO shared.list_item (list_type, code, label, parent_id) VALUES ('BETRIEB', '51', 'Betrieb 51 - Kompressor', (SELECT id FROM shared.list_item WHERE list_type='ANLAGE' AND code='LDB2'));
Nutzung in property_schema
Eigenschaften mit sql_type = 'FK_LIST' referenzieren shared.list_item über list_type:
-- In asset.property_schema: -- sql_type='FK_LIST', lookup_table='shared.list_item', lookup_key_col='code', -- lookup_disp_col='label', regex_pattern='ANLAGE' ← enthält list_type
Alternativ: neues Feld list_type_ref VARCHAR(50) NULL in asset.property_schema.