I QuestGame kan et QuestItem indeholde et billede. Entiteten har derfor blandt andet denne property:
public byte[]? Picture { get; set; }Code language: C# (cs)
Et billede består af binære data, og et byte[] kan derfor indeholde selve billedfilen. Det virker umiddelbart som en god og nem løsning.
Da vi bruger PostgreSQL gennem Npgsql og EF Core, vil et byte[] typisk blive mappet til PostgreSQL-typen bytea, der er beregnet til binære data.
Men selvom PostgreSQL er i stand til at gemme billeder, er spørgsmålet, om databasen bør have dette ansvar. I dette indlæg vil jeg diskutere hvornår det giver mening at gemme billeder i databasen, og hvornår man bør overlade det til eksternt filestorage.
Fordelen ved at gemme billedet i databasen
Den mest åbenlyse fordel ved byte[]-løsningen er enkelhed.
Billedet følger det QuestItem, det tilhører, og vi kan håndtere både almindelige data og billeddata gennem EF Core. Vi behøver ikke introducere endnu en service til filhåndtering.
Et QuestItem kunne eksempelvis indeholde:
QuestItem
├── Id
├── Title
├── Description
├── Position
└── Picture
Billedet bliver dermed en del af de data, vi allerede gemmer i PostgreSQL.
Det gør også sammenhængen mellem vores data simpel. Hvis databasen sikkerhedskopieres, kommer billederne med, og vi risikerer ikke umiddelbart at have en databasepost, der peger på en fil, som er blevet slettet et andet sted.
Til et mindre system eller et MVP kan det derfor være en gangbar løsning.
Problemet er mængden af data
Et billede fylder dog væsentligt mere end de fleste andre properties på et QuestItem.
En titel, status eller ID fylder meget lidt sammenlignet med et billede på måske flere megabyte. Hvis antallet af billeder vokser, kan databasen derfor også vokse markant.
Det bliver særligt interessant i QuestGame, fordi vi ikke nødvendigvis skal bruge billedet hver gang, vi henter et QuestItem.
Når spilleren åbner kortet, har klienten eksempelvis primært brug for information som:
- position
- type
- status
Hvis vi blot henter komplette entities, risikerer vi samtidig at hente billeddata, selvom billederne slet ikke skal bruges på kortet.
Her bliver projection relevant. Microsoft anbefaler generelt kun at hente de properties, man faktisk har brug for, og fremhæver store binære kolonner som noget, der kan medføre unødvendig datatransport.
Vi kunne eksempelvis skrive:
var items = await context.QuestItems
.Select(x => new QuestItemMapDto(
x.Id,
x.Position,
x.ElementType,
x.Status))
.ToListAsync();
Code language: C# (cs)
Her bliver Picture ikke hentet.
Det gør løsningen med byte[] mere realistisk, men billederne optager dog stadig meget plads i PostgreSQL og bliver en del af eksempelvis databasebackup.
Alternativet: Object Storage
En anden løsning er at gemme selve billedet uden for PostgreSQL og kun gemme en reference:
public string? PictureUrl { get; set; }Code language: C# (cs)
Billedet kunne eksempelvis ligge i Azure Blob Storage, Amazon S3 eller MinIO.
Object storage er specifikt designet til ustrukturerede data som billeder, dokumenter og andre filer. Microsoft beskriver eksempelvis Azure Blob Storage som en løsning optimeret til store mængder ustrukturerede data og blandt andet til levering af billeder og dokumenter.
Strukturen kunne så være:
QuestItem
├── Id
├── Position
├── ElementType
└── PictureUrl
│
└──> Object Storage
└── picture.jpg
Code language: JavaScript (javascript)
Det giver også en tydelig ansvarsfordeling: PostgreSQL håndterer vores strukturerede domænedata, mens et system designet til filer håndterer billederne.
Til gengæld får vi mere kompleksitet. Vi skal håndtere upload og sletning separat og sikre, at databasen og vores filstorage forbliver synkroniserede.
Hvad giver mening i QuestGame?
Vores nuværende løsning med byte[] er ikke i sig selv forkert, men den er ikke bæredygtig eller skalerbar.
QuestGame er dog “kun” et studieprojekt og et MVP, hvor vi først og fremmest skal demonstrere at vi kan forstå et forretningsdomæne og bygge et system derefter. At introducere object storage giver flere komponenter, som skal implementeres og vedligeholdes, og udgør samtidig en risiko for at vi bevæger os ud i en omgang ‘feature creep’.
Med det sagt, så vil jeg foretrække object storage til større eller “virkelige” projekter. Det kan også ske, at vi bruger det til QuestGame – og hvis ikke, vil det indgå i vores refleksioner omkring bæredygtighed.
Det centrale spørgsmål er derfor ikke:
Kan PostgreSQL gemme billeder?
Det kan den.
Det mere interessante systemarkitektoniske spørgsmål er:
Er PostgreSQL det mest hensigtsmæssige sted at gemme dem?
Og svaret afhænger i høj grad af systemets størrelse, krav og kompleksitet.


