SQL & Identifikatoren-Migration: steam/license zu citizenid
Komplette Anleitung zur Migration von ESX zu QBCore/QBOX: Crosswalk-Tabellen, Konten-Migration, Fahrzeuge und Validierung. Produktionsreifes SQL für 2026.

Anwendungsfall: Du migrierst von ESX zu QBCore oder QBOX
Anwendungsfall: Du migrierst von ESX zu QBCore oder QBOX (qbx_core) und benötigst eine saubere, nachvollziehbare Migration von Spieler-Identifikatoren und Guthaben. Dieser Leitfaden liefert produktionsreifes SQL, einen reversibler Plan und Validierungsschritte.
Dieser Leitfaden ist Teil unseres vollständigen FiveM-Framework-Guides, in dem wir ESX, QBCore und QBOX ausführlich vergleichen und dir bei der Wahl des richtigen Frameworks helfen.
Verwandte Artikel:
- Adapter-Patterns: ESX↔QBCore↔QBOX (Exports, Events & Player Models) — https://vertexmods.com/de/blog/adapter-patterns
- FiveM-Skripte konvertieren – ESX, QBCore, QBOX (Framework-Guide) — https://vertexmods.com/de/blog/converting-fivem-scripts/
Was sich zwischen ESX und QBCore/QBOX ändert
Thema
ESX (üblich)
QBCore / QBOX (üblich)
Primärer Spielerschlüssel
identifier (z. B. license:xxx oder steam:xxx)
citizenid (serverseitig generiertes Token)
Alternative Identifikatoren
users.identifier, manchmal separate identifiers-Tabelle
Spalten wie license, steam, fivem neben citizenid
Geldmodell
Separate Konten (cash/bank/black_money) via users.accounts (JSON) oder user_accounts-Zeilen
Einzelnes money-JSON in players (z. B. { "cash": 0, "bank": 5000 }); optionale zusätzliche Wallets
Fahrzeuge
owned_vehicles.owner verweist auf ESX identifier
player_vehicles.citizenid (oder license bei manchen Forks)
QBOX folgt grundsätzlich QBs DB-Schema. Behandle QBOX als „QB-Schema + qbx-Erweiterungen". Vergleiche immer dein Live-Schema.
Goldene Regeln (nicht überspringen)
- Schreibzugriffe einfrieren während der Migration (Spielserver und externe Bots stoppen).
- Vollständiges Backup und ein Dump der Tabellenstrukturen. Beides mit Zeitstempeln speichern.
- Transaktionsbasiert arbeiten pro Tabelle wenn möglich; Schritte idempotent halten.
- Crosswalk erstellen (
old_identifier→citizenid), den du wiederverwenden oder zurückrollen kannst.
Zielzustand (QB/QBOX-Baseline)
Eine typische players-Tabelle (Spalten variieren je Fork):
-- Tatsächliches Schema prüfen und anpassen.
DESCRIBE players; -- Erwarte Spalten wie: citizenid, license, name, money, charinfo, job, gang, metadata
- citizenid: Primärschlüssel in QB/QBOX.
- license/steam: Für Forensik und Re-Linking behalten.
- money (JSON): z. B.
{"cash":123,"bank":456}. Manche Server ergänzencrypto,dirtyetc.
Schritt 0 — Snapshot & Staging
# MySQL/MariaDB-Backup mysqldump -u root -p --routines --triggers yourdb > yourdb_$(date +%F_%H%M).sql
Optional: Nur-Schema-Snapshot
mysqldump -u root -p --no-data yourdb > yourdb_schema_$(date +%F_%H%M).sql
Staging-Kopie aufsetzen
Eine Staging-Kopie aufsetzen. Zuerst alles dort testen.
Schritt 1 — Crosswalk-Tabelle erstellen
Wir ordnen jeden ESX-identifier einer neuen citizenid zu. Falls bereits eine players-Tabelle mit citizenids existiert, wird die Zuordnung umgekehrt (siehe Hinweis Bestehende QB-Spieler unten).
-- 1) Crosswalk erstellen
CREATE TABLE IF NOT EXISTS identifier_crosswalk (
old_identifier VARCHAR(60) PRIMARY KEY, citizenid VARCHAR(20) NOT NULL, license VARCHAR(60) NULL, steam VARCHAR(60) NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2) Aus ESX-Users befüllen (Tabellen-/Spaltennamen an dein ESX anpassen)
INSERT IGNORE INTO identifier_crosswalk (old_identifier, license, steam, citizenid)
SELECT u.identifier AS old_identifier, CASE WHEN u.identifier LIKE 'license:%' THEN u.identifier ELSE NULL END AS license, CASE WHEN u.identifier LIKE 'steam:%' THEN u.identifier ELSE NULL END AS steam, UPPER(SUBSTRING(REPLACE(UUID(),'-',''),1,10)) AS citizenid FROM users u;
-- 3) Falls separate \`identifiers\`-Tabelle vorhanden, beste bekannte Werte zusammenführen
UPDATE identifier_crosswalk x
JOIN (
SELECT i1.identifier AS old_identifier,
MAX(CASE WHEN i1.type='license' THEN i1.value END) AS license,
MAX(CASE WHEN i1.type='steam' THEN i1.value END) AS steam
FROM identifiers i1 GROUP BY i1.identifier
) i ON i.old_identifier = x.old_identifier
SET x.license = COALESCE(i.license, x.license),
x.steam = COALESCE(i.steam, x.steam);
-- 4) Eindeutigkeit & Indizes
ALTER TABLE identifier_crosswalk
ADD UNIQUE KEY ux_cid (citizenid), ADD KEY ix_license (license), ADD KEY ix_steam (steam);
Bestehende QB-Spieler? Falls bereits
players-Zeilen vorhanden sind, erstelle den Crosswalk durch Selektion vonlicense/steamund der bestehendencitizenidanstatt neue zu generieren. Der Crosswalk darf einem bestehenden QB-Spieler niemals eine neue citizenid zuweisen.
Schritt 2 — Ziel-players-Zeilen vorbereiten
Fehlende players-Zeilen basierend auf ESX-users erstellen.
INSERT INTO players (citizenid, license, name, money, charinfo, metadata)
SELECT x.citizenid, COALESCE(NULLIF(x.license,''), NULLIF(x.steam,'')) AS license_like, TRIM(CONCAT(COALESCE(u.firstname,''), ' ', COALESCE(u.lastname,''))) AS name_like,
'{"cash":0,"bank":0}' AS money,
CONCAT('{',
'"firstName":"', REPLACE(COALESCE(u.firstname,''),'"','\\"'), '",',
'"lastName":"', REPLACE(COALESCE(u.lastname,''),'"','\\"'), '",',
'"birthdate":"', REPLACE(COALESCE(u.dateofbirth,''),'"','\\"'),'",',
'"gender":"', REPLACE(COALESCE(u.sex,''),'"','\\"'), '"',
'}') AS charinfo,
CONCAT('{',
'"esx_identifier":"', REPLACE(u.identifier,'"','\\"'), '"',
'}') AS metadata
FROM users u JOIN identifier_crosswalk x ON x.old_identifier = u.identifier LEFT JOIN players p ON p.citizenid = x.citizenid WHERE p.citizenid IS NULL;
Schritt 3 — Konten → Money migrieren
A) ESX speichert Guthaben in users.accounts JSON
-- Beispiel: users.accounts = '{"bank":5000, "money":750, "black_money":200}'
CREATE TEMPORARY TABLE esx_balances AS
SELECT u.identifier, COALESCE(JSON_EXTRACT(u.accounts, '$.money'), 0) AS esx_cash, COALESCE(JSON_EXTRACT(u.accounts, '$.bank'), 0) AS esx_bank, COALESCE(JSON_EXTRACT(u.accounts, '$.black_money'), 0) AS esx_black FROM users u;
UPDATE players p
JOIN identifier_crosswalk x ON x.citizenid = p.citizenid JOIN esx_balances b ON b.identifier = x.old_identifier SET p.money = JSON_OBJECT(
'cash', CAST(b.esx_cash AS UNSIGNED),
'bank', CAST(b.esx_bank AS UNSIGNED)
);
B) ESX speichert Guthaben in user_accounts-Zeilen
CREATE TEMPORARY TABLE esx_balances AS
SELECT ua.identifier,
SUM(CASE WHEN ua.account='money' THEN ua.money ELSE 0 END) AS esx_cash,
SUM(CASE WHEN ua.account='bank' THEN ua.money ELSE 0 END) AS esx_bank,
SUM(CASE WHEN ua.account='black_money' THEN ua.money ELSE 0 END) AS esx_black
FROM user_accounts ua GROUP BY ua.identifier;
Umgang mit black_money (eine Option wählen)
- Option 1 (empfohlen): Eigenes Wallet-Key im QB-money-JSON erstellen, z. B.
"dirty". - Option 2: In Items umwandeln (z. B. markierte Scheine) — erfordert Item-Migration; nicht Thema dieses Guides.
- Option 3: Auf null setzen (dringend abgeraten, außer du hast einen Wipe angekündigt).
Schritt 4 — Fremdtabellen mit ESX-identifier neu verknüpfen
Typische Tabellen:
owned_vehicles.owner→ aufcitizenidmappen (QB:player_vehicles.citizenid)- Alle custom Tabellen mit
identifier-Spalten (Häuser, Abrechnungen, Gangs, Betriebe)
Fahrzeuge (ESX → QB)
ALTER TABLE owned_vehicles ADD COLUMN citizenid VARCHAR(20) NULL;
UPDATE owned_vehicles v
JOIN identifier_crosswalk x ON x.old_identifier = v.owner SET v.citizenid = x.citizenid WHERE v.citizenid IS NULL;
CREATE INDEX ix_ov_cid ON owned_vehicles (citizenid);
INSERT IGNORE INTO player_vehicles (citizenid, plate, vehicle, state, garage)
SELECT x.citizenid, v.plate, v.vehicle, 0 AS state, 'A' AS garage
FROM owned_vehicles v JOIN identifier_crosswalk x ON x.old_identifier = v.owner;
Schritt 5 — Constraints, Indizes und Integritätsprüfungen
ALTER TABLE players
ADD UNIQUE KEY ux_players_citizenid (citizenid);
ALTER TABLE players
ADD KEY ix_players_license (license);
-- Verwaiste Crosswalk-Einträge (keine players-Zeile)
SELECT x.*
FROM identifier_crosswalk x LEFT JOIN players p ON p.citizenid = x.citizenid WHERE p.citizenid IS NULL;
Schritt 6 — Validierungs-Suite
- Zeilenzählungen:
COUNT(users)≈COUNT(players)(innerhalb erwarteter Abweichungen). - Guthabensummen: Summe ESX cash/bank ≈ Summe QB-Wallets nach Migration.
- Stichprobenprüfung: 10 Spieler nach Name auswählen; citizenid, Guthaben, Fahrzeuge prüfen.
- Login-Test: Server im Staging hochfahren; einige bekannte Spieler einloggen; UIs prüfen.
Schritt 7 — Laufzeit-Kompatibilität (Adapter)
Auch nach der Migration referenzieren manche Legacy-Skripte noch ESX-identifier. Den Crosswalk behalten und einen Helper nutzen, um zur Laufzeit aufzulösen.
Lua-Helper (Server):
--- lookup_citizenid.lua
local function getCitizenIdByIdentifier(identifier)
local result = MySQL.query.await('SELECT citizenid FROM identifier_crosswalk WHERE old_identifier = ? LIMIT 1', { identifier })
if result and result[1] then return result[1].citizenid end
return nil
end
return { getCitizenIdByIdentifier = getCitizenIdByIdentifier }
Diesen in Legacy-Event-Handlern verwenden, bis alle Skripte QB/QBOX-nativ sind. Vollständige Interface-Shims im Adapter-Patterns-Artikel.
- Adapter-Patterns: https://vertexmods.com/de/blog/adapter-patterns
- Vollständiger Konvertierungsleitfaden: https://vertexmods.com/de/blog/converting-fivem-scripts/
Rollback-Strategie
identifier_crosswalkund ein Pre-Migrations-Backup behalten.- Falls etwas schiefläuft, neu erstellte
players-Zeilen löschen und Backup wiederherstellen. - Migration nach Behebung von Datenproblemen erneut starten.
-- Neue Zeilen markieren
UPDATE players SET metadata = JSON_MERGE_PATCH(COALESCE(metadata,'{}'), JSON_OBJECT('migration_tag','esx_to_qb_2025_08_16'))
WHERE citizenid IN (SELECT citizenid FROM identifier_crosswalk);
Randfälle & Tipps
- Mehrere Charaktere pro Spieler: Falls dein ESX einen
identifierpro Account nutzte (kein Multi-Char), aber du QB mit Multi-Char planst, zusätzliche Citizens später über In-Game-Flows generieren, nicht hier. - Namenskollisionen: Zwei ESX-User mit gleichem Vor-/Nachnamen sind kein Problem; citizenid ist der Schlüssel.
- Fehlende Identifier-Werte: Den stabilsten verfügbaren Identifier bevorzugen (
steam,license2,fivem). - Altes MySQL ohne JSON: Plain-Text-JSON-Strings verwenden und in App-Code parsen; Upgrade planen.
- Black-Money-Richtlinie: Entscheidung kommunizieren.
FAQ
F: Kann ich ESX-identifier irgendwo weiter nutzen?
A: Ja, aber als Legacy behandeln. Den Crosswalk zur Auflösung nutzen und Skripte so schnell wie möglich auf citizenid umstellen.
F: Braucht QBOX anderes SQL? A: Nicht für Identifikatoren/Guthaben; QBOX folgt QBs Schema eng. Spaltennamen vor dem Ausführen validieren.
F: Was ist mit Inventaren, Jobs, Gangs? A: Außerhalb des Rahmens dieses Artikels. Nach Stabilisierung von Identifikatoren/Guthaben angehen.
Nächste Schritte
- Laufzeit-Shims aus Adapter-Patterns implementieren: https://vertexmods.com/de/blog/adapter-patterns
- Vollständige Migration mit dem Framework-Guide abschließen: https://vertexmods.com/de/blog/converting-fivem-scripts/
- Lokale Abweichungen (custom Wallets, extra Spalten) im Repo dokumentieren.
Anhang — Idempotente Wrapper
Kritische UPDATE/INSERT-Operationen mit Guards umhüllen, damit sie sicher mehrfach ausgeführt werden können.
-- Beispiel-Guard: Nur Spieler mit unverändertem Guthaben updaten
UPDATE players p
JOIN identifier_crosswalk x ON x.citizenid = p.citizenid JOIN esx_balances b ON b.identifier = x.old_identifier SET p.money = JSON_OBJECT('cash', CAST(b.esx_cash AS UNSIGNED), 'bank', CAST(b.esx_bank AS UNSIGNED)) WHERE JSON_EXTRACT(p.money, '$.cash') = 0 AND JSON_EXTRACT(p.money, '$.bank') = 0;
Den Crosswalk dauerhaft behalten. Er ist dein Rosetta Stone für alte Logs und Skripte.


