Creare un'entità persistente
Questo tutorial crea un CharacterProfile, lo memorizza in PostgreSQL e lo legge,
aggiorna, interroga ed elimina tramite IDataAccess<CharacterProfile>. Moongate
gestisce internamente i moduli di persistenza.
L’esempio eseguibile usa il checkout corrente del sorgente Moongate e un’applicazione console standalone. L’ultimo passaggio mostra come registrare la stessa entità in un plugin Moongate.
1. Creare il progetto di esempio
Section titled “1. Creare il progetto di esempio”Servono l’SDK .NET 10, un checkout Moongate e un database PostgreSQL vuoto. Il ruolo database dell’esempio deve poter creare schemi, tabelle e sequenze. Crea prima il database: Moongate crea schema e tabella dell’entità, non il database stesso.
Dalla radice del repository Moongate, crea un progetto affiancato e aggiungi un riferimento alla libreria di persistenza:
dotnet new console --framework net10.0 --name EntityTutorial --output ../Moongate.EntityTutorialdotnet add ../Moongate.EntityTutorial/EntityTutorial.csproj reference src/Moongate.Persistence/Moongate.Persistence.csprojIl riferimento al progetto mantiene il tutorial sulla stessa versione API del checkout. Fornisce anche gli attributi FreeSql, il container DryIoc e i tipi Moongate Core usati sotto.
2. Definire l’entità
Section titled “2. Definire l’entità”Crea CharacterProfile.cs nel nuovo progetto:
using FreeSql.DataAnnotations;using Moongate.Core.Interfaces.Entities;using Moongate.Core.Primitives;
namespace EntityTutorial;
[Table(Name = "tutorial_entities.character_profiles")]public sealed class CharacterProfile : IMoongateEntity{ [Column(Name = "id", IsPrimary = true, MapType = typeof(long))] public Serial Id { get; set; }
[Column(Name = "name", StringLength = 100)] public string Name { get; set; } = "";
[Column(Name = "level")] public int Level { get; set; } = 1;}| Dichiarazione | Scopo |
|---|---|
IMoongateEntity |
Fornisce alla persistenza un’identità comune Serial Id. |
Table(Name = "tutorial_entities.character_profiles") |
Seleziona schema e tabella PostgreSQL. Usa nomi espliciti minuscoli. |
IsPrimary = true, MapType = typeof(long) |
Memorizza il Serial come chiave primaria PostgreSQL bigint. |
Column(Name = ...) |
Mantiene stabili i nomi delle colonne database quando cambiano i nomi C#. |
StringLength = 100 |
Dichiara la lunghezza massima del nome memorizzato. |
Lascia Id al valore predefinito (Serial.Zero) per una nuova entità. UpsertAsync
riserva un ID dalla sequenza PostgreSQL della tabella, inserisce l’entità e riscrive
l’ID nello stesso oggetto. Non servono un nome di sequenza o ulteriori dipendenze da
servizi. Mantieni un setter Id pubblico e non usare IsIdentity = true. Un ID
esplicito diverso da zero viene mantenuto e usa il normale inserimento o aggiornamento.
I nomi delle colonne sono normalmente snake_case minuscolo: Username diventa
username, HashPassword diventa hash_password e CreatedAt diventa created_at.
I mapping espliciti [Column(Name = "...")] prevalgono e devono anch’essi usare
snake_case minuscolo. Mantieni espliciti [Table(Name = "...")] qualificato con lo
schema e il mapping dell’identità.
L’esempio iniziale usa proprietà scalari. Per una proprietà che deve restare in memoria,
usa [Column(IsIgnore = true)]. Valori personalizzati e collezioni possono usare JSONB
con [JsonMap]; vedi Memorizzare valori personalizzati come JSONB.
Un grafo di oggetti senza annotazioni non viene serializzato automaticamente.
3. Registrare l’entità e usare il suo accesso ai dati
Section titled “3. Registrare l’entità e usare il suo accesso ai dati”Sostituisci il Program.cs generato con questo programma completo:
using DryIoc;using EntityTutorial;using Moongate.Core.Primitives;using Moongate.Persistence.Data.Config;using Moongate.Persistence.Extensions;using Moongate.Persistence.Interfaces;using Moongate.Persistence.Services;using Moongate.Persistence.Types.Persistence;
var connectionString = Environment.GetEnvironmentVariable("MOONGATE_TUTORIAL_DATABASE") ?? throw new InvalidOperationException("Set MOONGATE_TUTORIAL_DATABASE before running the tutorial.");
var options = new PostgreSqlPersistenceOptions( [new PersistenceDatabaseOptions(PersistenceDatabaseTarget.Realm, connectionString)], autoSynchronizeSchema: true);
using var container = new Container();container.RegisterMoongatePersistence(options) .AddPersistenceWorld<CharacterProfile>();
await using var persistence = container.Resolve<MoongatePersistenceService>();await persistence.InitializeAsync();
var profiles = container.Resolve<IDataAccess<CharacterProfile>>();var profile = new CharacterProfile{ Name = "Mario", Level = 1};
await profiles.UpsertAsync(profile); // profile.Id is now assigned.
var loaded = await profiles.GetByIdAsync(profile.Id) ?? throw new InvalidOperationException("The saved profile was not found.");Console.WriteLine($"Created: {loaded.Name}, level {loaded.Level}");
loaded.Level = 2;await profiles.UpsertAsync(loaded);
var updated = await profiles.GetByIdAsync(profile.Id) ?? throw new InvalidOperationException("The updated profile was not found.");Console.WriteLine($"Updated: {updated.Name}, level {updated.Level}");
var page = await profiles.QueryAsync(value => value.Level >= 2, skip: 0, take: 20);Console.WriteLine($"Profiles at level 2 or higher: {page.Count}");
var deleted = await profiles.DeleteAsync(profile.Id);Console.WriteLine($"Deleted: {deleted}");Console.WriteLine($"Missing after delete: {await profiles.GetByIdAsync(profile.Id) is null}");AddPersistenceWorld<CharacterProfile>() seleziona il database Realm. Moongate
raggruppa le entità registrate per destinazione e schema in moduli interni; non serve
una classe IPersistenceModule. Registra tutto prima dell’inizializzazione o
dell’anteprima dello schema, perché la preparazione dello schema congela le registrazioni.
Questo esempio standalone abilita esplicitamente la sincronizzazione dello schema,
così InitializeAsync() crea la tabella del tutorial. Usa questa impostazione per
il database di sviluppo vuoto dell’esercizio. I normali deployment Moongate hanno
la sincronizzazione dello schema disabilitata e usano il flusso revisione/applicazione
del passaggio 5.
4. Eseguire e controllare il risultato
Section titled “4. Eseguire e controllare il risultato”Imposta MOONGATE_TUTORIAL_DATABASE tramite ambiente o gestore dei segreti sull’URI
di connessione del database vuoto. La sua forma è:
postgres://USER:PASSWORD@HOST:5432/moongate_tutorialSostituisci i segnaposto con i dettagli della connessione e codifica percentualmente
i caratteri riservati nelle credenziali, per esempio @ come %40. Tieni le credenziali
effettive fuori dai file sorgente. Vedi Configurazione delle connessioni
per le opzioni URI e l’espansione dell’ambiente nel TOML del server.
Dalla radice del repository, esegui:
dotnet run --project ../Moongate.EntityTutorial/EntityTutorial.csprojIl programma stampa:
Created: Mario, level 1Updated: Mario, level 2Profiles at level 2 or higher: 1Deleted: TrueMissing after delete: TrueLa tabella rimane in PostgreSQL; alla fine l’esempio elimina solo la propria riga del profilo. Le operazioni illustrano il contratto della persistenza:
UpsertAsyncinserisce un ID mancante o aggiorna la riga con quell’ID. Le scritture indipendenti eseguono il commit prima che il task restituito si completi.- Le letture restituiscono valori scollegati.
loaded.Level = 2modifica solo quell’oggetto; il secondoUpsertAsyncpersiste la modifica. QueryAsynctraduce il predicato in SQL. L’overload paginato limita il risultato e lo ordina per identità; le espressioni non supportate falliscono esplicitamente.DeleteAsyncrimuove una riga e restituisce se esisteva. UnGetByIdAsyncsuccessivo restituiscenull.
Usa le transazioni quando più scritture sulla stessa destinazione devono essere confermate insieme. Una transazione non può attraversare Accounts e Realm.
5. Usare l’entità in un plugin Moongate
Section titled “5. Usare l’entità in un plugin Moongate”Sposta l’entità nella directory Data del plugin e aggiornane il namespace.
Fai riferimento alla versione corrispondente di Moongate.Persistence come descritto
in Scrivere un plugin.
Nel metodo Register(Container container) del plugin esistente, registra l’entità
usando Moongate.Persistence.Extensions:
public void Register(Container container){ container.AddPersistenceWorld<CharacterProfile>();}L’host gestisce già registrazione, inizializzazione e rilascio della persistenza.
Il plugin registra i tipi di entità e lascia che i suoi servizi ricevano
IDataAccess<CharacterProfile> tramite injection nel costruttore. Non eseguire
l’inizializzazione o le operazioni database dell’esempio console dentro Register.
Scegli l’helper secondo il proprietario dei dati:
| Helper | Database e dati previsti |
|---|---|
AddPersistenceWorld<TEntity>() |
Database di questo realm: personaggi, oggetti e stato del mondo. |
AddPersistenceAuth<TEntity>() |
Database Accounts condiviso: dati degli account e di autenticazione. |
Registra un tipo una volta, in una sola destinazione per container. L’helper sceglie il database; l’attributo della tabella sceglie lo schema al suo interno. Questi helper instradano l’accesso al database nel processo corrente; non implementano comunicazione tra server login e game.
Per l’entità del mondo sopra, imposta la connessione del realm in
config/moongate.toml dell’host:
[persistence]auto_sync_schema = false
[persistence.realm]connection_string = "$MOONGATE_REALM_DATABASE"Imposta quella variabile sull’URI PostgreSQL del realm. Un host game controlla solo
il proprio database Realm; standalone richiede anche [persistence.accounts].
I valori predefiniti sono database locali auth e world con moongate / moongate.
I comandi di anteprima/generazione dello schema si connettono solo alle destinazioni
necessarie per i mapping. Nel deployment, mantieni la sincronizzazione automatica
disabilitata e distribuisci una migrazione SQL versionata con il plugin.
-
Distribuisci il plugin in una radice di riferimento il cui database abbia lo schema precedente.
-
Genera una bozza per World, scegliendo il numero successivo della sequenza del componente:
Terminal window mgserver --root-directory /srv/moongate/reference \--persistence-schema generate --migration-target world \--migration-output ./MyPlugin/migrations/world/0001_create_characters.sql -
Revisiona l’SQL e aggiungi
MyPlugin/migrations/manifest.json:{ "id": "my-plugin" } -
Includi
migrations/**/*nell’output pubblicato del plugin e committalo con l’entità. Arresta il realm di destinazione e applica usando una connessione con ruolo schema:Terminal window ./mgctl migrate apply \--root-directory /srv/moongate/realm-1 --target world -
Avvia il server con la connessione runtime. Verifica sia la cronologia delle migrazioni sia lo schema mappato prima di avviare i servizi.
Dove sono i file? L’SQL core è in migrations/auth e migrations/world accanto
al server. L’SQL dei plugin è nella directory migrations/ di ogni bundle. Il runner
registra file riusciti e checksum in moongate_migrations.history. Non modificare
mai un file applicato: aggiungi una nuova migrazione numerata. Genera sullo schema
precedente, non su un database già aggiornato. La bozza include tutti i moduli
registrati per la destinazione, quindi usa una radice di riferimento isolata per il
plugin e verifica la proprietà.
Il precedente esempio console usa intenzionalmente la sincronizzazione automatica per un database temporaneo. Questa comodità non crea file SQL o cronologia delle versioni. Usa la guida alle operazioni sullo schema per deployment di tipo produttivo, database di riferimento, ruoli e gestione degli errori.
La semplice registrazione dell’entità abilita letture e scritture esplicite. Per
oggetti vivi mantenuti dal game loop, l’overload con sorgente e snapshot di
AddPersistenceWorld li include anche in SaveAllAsync e nei salvataggi del mondo
dell’host. Segui Snapshot del mondo vivo per
registrare un clone scollegato e catturarlo tramite il loop proprietario. Rimuovere
un oggetto dalla memoria non elimina la sua riga; l’eliminazione rimane esplicita.
6. Generare migrazioni automaticamente durante lo sviluppo
Section titled “6. Generare migrazioni automaticamente durante lo sviluppo”Quando l’entità è registrata con AddPersistenceAuth<TEntity>() o
AddPersistenceWorld<TEntity>(), abilita il flusso di sviluppo:
[persistence]auto_sync_schema = falseauto_generate_migrations = truemigrations_directory = "${MOONGATE_ROOT}/migrations"Imposta MOONGATE_ROOT sulla radice dei dati del server, o usa una directory sorgente
assoluta. Le migrazioni auth core distribuite (da 0001 a 0004) devono già essere
applicate, perciò i file generati sotto partono da 0005. Per una nuova entità
personalizzata registrata con AddPersistenceAuth<CustomAuthEntity>():
- Avvia il server con la nuova entità registrata. L’avvio scrive
migrations/auth/0005_auto_schema.sql, lo applica e ne registra il checksum. - Arresta il server e aggiungi
public DateTime? LastLoginAt { get; set; }all’entità. - Riavvia. L’avvio scrive e applica
0006_auto_schema.sql; le righe esistenti ricevono un valorelast_login_atnull. - Riavvia senza modificare l’entità: non viene generata una nuova migrazione.
- Committa entrambi i file SQL generati e il codice dell’entità. I numeri proseguono sempre dopo la migrazione più alta esistente nel componente.
Gli indici dichiarati con [Index(...)] sono automatici per una tabella appena
creata quando usano colonne semplici, compresi indici univoci e compositi. Aggiungere
un indice a una tabella esistente richiede revisione: i dati esistenti potrebbero
violare un indice univoco.
Una modifica che richiede revisione lascia una bozza SQL contrassegnata e interrompe
l’avvio. Revisiona il file non applicato e rimuovi -- moongate:review-required prima
di riavviare. I file esistenti applicati sono immutabili. Lascia disabilitati i flag
di sviluppo nel deployment e usa il runner standalone delle migrazioni per i file revisionati.
Vedi Migrazioni automatiche di sviluppo per directory dei plugin, valori predefiniti delle colonne obbligatorie, baseline dei database esistenti e recupero dagli errori.
7. Memorizzare valori personalizzati come JSONB
Section titled “7. Memorizzare valori personalizzati come JSONB”Usa una colonna JSONB per piccoli valori posseduti dall’entità, come progressi delle quest o preferenze. Usa entità/tabelle separate per oggetti con identità e ciclo di vita propri, come oggetti dell’inventario con un Serial.
Moongate include FreeSql.Extensions.JsonMap e lo abilita prima del mapping di ogni
database Accounts o Realm. Aggiungi questa proprietà a CharacterProfile:
[JsonMap, Column(Name = "quest_progress", DbType = "jsonb", IsNullable = true)]public List<QuestProgress>? QuestProgress { get; set; } = [];JsonMap e Column usano FreeSql.DataAnnotations. Crea QuestProgress.cs nel
progetto del tutorial (usa il namespace Data del plugin quando lo sposti lì):
namespace EntityTutorial;
public sealed class QuestProgress{ public int QuestId { get; set; } public bool Completed { get; set; } public List<int> Milestones { get; set; } = [];}QuestProgress è un valore nel profilo: non richiede IMoongateEntity, un ID o una
propria registrazione di persistenza. Gli stessi attributi supportano una proprietà
oggetto personalizzata al posto di una lista.
Dopo aver applicato la migrazione della colonna, salva tramite l’accesso ai dati esistente:
var profile = new CharacterProfile{ Name = "Mario", QuestProgress = [ new() { QuestId = 10, Completed = false, Milestones = [1, 2] }, new() { QuestId = 25, Completed = true } ]};await profiles.UpsertAsync(profile);La colonna quest_progress contiene un array JSON, non righe figlie separate:
[ { "QuestId": 10, "Completed": false, "Milestones": [1, 2] }, { "QuestId": 25, "Completed": true, "Milestones": [] }]L’estensione usa Newtonsoft.Json. Con le impostazioni predefinite del serializzatore, i nomi dei membri JSON mantengono le maiuscole/minuscole C#; la convenzione snake_case SQL si applica al nome della colonna, non ai membri nel documento. Mantieni stabili i nomi JSON memorizzati; cambiare il nome di un membro DTO non genera una migrazione di colonna SQL né trasforma automaticamente i documenti esistenti. Gli attributi System.Text.Json non configurano questo serializzatore.
Una lista vuota viene memorizzata come []. Con IsNullable = true, una proprietà
null è SQL NULL e viene riletta come null. = [] inizializza le nuove istanze C#;
non garantisce che i null del database vengano convertiti in liste vuote.
Le letture ricostruiscono valori scollegati. Modificare una lista o un elemento non
scrive in PostgreSQL finché non chiami UpsertAsync, o un salvataggio del mondo
registrato non lo cattura. Un normale upsert sostituisce l’intero valore JSON
memorizzato; non esiste un’unione automatica per elemento. Gli aggiornamenti concorrenti
seguono il normale comportamento dell’ultima scrittura che prevale.
Per i salvataggi del mondo vivo, il callback dello snapshot deve copiare sia la lista
sia ogni valore mutabile al suo interno, comprese liste annidate come Milestones.
Copiare solo l’entità o usare new List<QuestProgress>(live.QuestProgress) continua
a condividere gli oggetti delle quest con il mondo vivo.
Il normale flusso delle migrazioni si applica anche alle colonne JSONB. Quando la
generazione delle migrazioni di sviluppo aggiunge una colonna JSONB nullable senza
un valore predefinito SQL dichiarato, le righe esistenti rimangono SQL NULL, anche
se la proprietà C# parte con = []. Moongate rimuove in questo caso il riempimento
implicito di FreeSql con oggetto/array vuoto, quindi non attiva trigger di aggiornamento
sulle righe esistenti. Revisiona l’SQL generato tramite altri flussi dello schema prima di applicarlo.
Aggiungere o cambiare una colonna è una modifica dello schema; cambiare solo la forma del payload JSON richiede una strategia esplicita di compatibilità o una migrazione dati revisionata. La memorizzazione JSONB non implica che ogni operazione LINQ su collezioni annidate venga tradotta in SQL.
Creato con ❤️ da Tom
