From 316881266b8a129f645e39a1fdc157db8db13612 Mon Sep 17 00:00:00 2001 From: Timothy Schenk Date: Thu, 16 Nov 2023 12:06:36 +0100 Subject: [PATCH] feat: dbcontext pooling & lazy loading --- Server/DB/Documents/Account.cs | 5 +- Server/DB/Documents/Character.cs | 6 + Server/DB/Documents/GuildMember.cs | 4 +- Server/DB/Documents/InventoryItem.cs | 2 +- ...DBPoolingAndLazyLoadingSupport.Designer.cs | 333 ++++++++++++++++++ ...16110504_DBPoolingAndLazyLoadingSupport.cs | 57 +++ .../WonderkingContextModelSnapshot.cs | 13 +- Server/DB/WonderkingContext.cs | 17 +- Server/Program.cs | 17 +- Server/Server.csproj | 1 + 10 files changed, 433 insertions(+), 22 deletions(-) create mode 100644 Server/DB/Migrations/20231116110504_DBPoolingAndLazyLoadingSupport.Designer.cs create mode 100644 Server/DB/Migrations/20231116110504_DBPoolingAndLazyLoadingSupport.cs diff --git a/Server/DB/Documents/Account.cs b/Server/DB/Documents/Account.cs index 20c8bc0..80ede30 100644 --- a/Server/DB/Documents/Account.cs +++ b/Server/DB/Documents/Account.cs @@ -20,7 +20,10 @@ public class Account [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; set; } - [Column(TypeName = "varchar(20)")] public string Username { get; set; } + [Column(TypeName = "varchar(20)")] + [MaxLength(20)] + public string Username { get; set; } + [Column(TypeName = "bytea")] public byte[] Password { get; set; } [EmailAddress] public string Email { get; set; } diff --git a/Server/DB/Documents/Character.cs b/Server/DB/Documents/Character.cs index c286fff..16e18e6 100644 --- a/Server/DB/Documents/Character.cs +++ b/Server/DB/Documents/Character.cs @@ -1,10 +1,12 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; using Wonderking.Game.Data.Character; using Wonderking.Packets.Outgoing.Data; namespace Server.DB.Documents; +[Index(nameof(Name), IsUnique = true), Index(nameof(Id), IsUnique = true)] public class Character { public virtual Account Account { get; set; } @@ -14,7 +16,11 @@ public class Character public Guid Id { get; set; } public ushort MapId { get; set; } + + [Column(TypeName = "varchar(20)")] + [MaxLength(20)] public string Name { get; set; } + public short LastXCoordinate { get; set; } public short LastYCoordinate { get; set; } public PvPLevel PvPLevel { get; set; } diff --git a/Server/DB/Documents/GuildMember.cs b/Server/DB/Documents/GuildMember.cs index 557aa2d..07e0edd 100644 --- a/Server/DB/Documents/GuildMember.cs +++ b/Server/DB/Documents/GuildMember.cs @@ -9,7 +9,7 @@ public class GuildMember [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; set; } - public Character Character { get; set; } - public Guild Guild { get; set; } + public virtual Character Character { get; set; } + public virtual Guild Guild { get; set; } public GuildRank Rank { get; set; } } diff --git a/Server/DB/Documents/InventoryItem.cs b/Server/DB/Documents/InventoryItem.cs index 08f5078..57754f0 100644 --- a/Server/DB/Documents/InventoryItem.cs +++ b/Server/DB/Documents/InventoryItem.cs @@ -3,7 +3,7 @@ namespace Server.DB.Documents; public class InventoryItem { public Guid CharacterId { get; set; } - public Character Character { get; set; } + public virtual Character Character { get; set; } public Guid Id { get; set; } public ushort ItemId { get; set; } public ushort Count { get; set; } diff --git a/Server/DB/Migrations/20231116110504_DBPoolingAndLazyLoadingSupport.Designer.cs b/Server/DB/Migrations/20231116110504_DBPoolingAndLazyLoadingSupport.Designer.cs new file mode 100644 index 0000000..932bf0b --- /dev/null +++ b/Server/DB/Migrations/20231116110504_DBPoolingAndLazyLoadingSupport.Designer.cs @@ -0,0 +1,333 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Server.DB; + +#nullable disable + +namespace Server.DB.Migrations +{ + [DbContext(typeof(WonderkingContext))] + [Migration("20231116110504_DBPoolingAndLazyLoadingSupport")] + partial class DBPoolingAndLazyLoadingSupport + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Server.DB.Documents.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Password") + .HasColumnType("bytea"); + + b.Property("PermissionLevel") + .HasColumnType("smallint"); + + b.Property("Salt") + .HasColumnType("bytea"); + + b.Property("Username") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Server.DB.Documents.Character", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("Experience") + .HasColumnType("bigint"); + + b.Property("Gender") + .HasColumnType("smallint"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("Health") + .HasColumnType("integer"); + + b.Property("LastXCoordinate") + .HasColumnType("smallint"); + + b.Property("LastYCoordinate") + .HasColumnType("smallint"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("Mana") + .HasColumnType("integer"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("Name") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("PvPLevel") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("GuildId"); + + b.HasIndex("Id") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Characters"); + }); + + modelBuilder.Entity("Server.DB.Documents.Guild", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Notice") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Guild"); + }); + + modelBuilder.Entity("Server.DB.Documents.GuildMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("Rank") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("GuildId"); + + b.ToTable("GuildMember"); + }); + + modelBuilder.Entity("Server.DB.Documents.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddOption") + .HasColumnType("smallint"); + + b.Property("AddOption2") + .HasColumnType("smallint"); + + b.Property("AddOption3") + .HasColumnType("smallint"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("Count") + .HasColumnType("integer"); + + b.Property("InventoryTab") + .HasColumnType("smallint"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("Option") + .HasColumnType("smallint"); + + b.Property("Option2") + .HasColumnType("smallint"); + + b.Property("Option3") + .HasColumnType("smallint"); + + b.Property("Rarity") + .HasColumnType("smallint"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.ToTable("InventoryItem"); + }); + + modelBuilder.Entity("Server.DB.Documents.Character", b => + { + b.HasOne("Server.DB.Documents.Account", "Account") + .WithMany("Characters") + .HasForeignKey("AccountId"); + + b.HasOne("Server.DB.Documents.Guild", "Guild") + .WithMany() + .HasForeignKey("GuildId"); + + b.OwnsOne("Wonderking.Packets.Outgoing.Data.BaseStats", "BaseStats", b1 => + { + b1.Property("CharacterId") + .HasColumnType("uuid"); + + b1.Property("Dexterity") + .HasColumnType("smallint"); + + b1.Property("Intelligence") + .HasColumnType("smallint"); + + b1.Property("Luck") + .HasColumnType("smallint"); + + b1.Property("Strength") + .HasColumnType("smallint"); + + b1.Property("Vitality") + .HasColumnType("smallint"); + + b1.Property("Wisdom") + .HasColumnType("smallint"); + + b1.HasKey("CharacterId"); + + b1.ToTable("Characters"); + + b1.WithOwner() + .HasForeignKey("CharacterId"); + }); + + b.OwnsOne("Wonderking.Packets.Outgoing.Data.JobData", "JobData", b1 => + { + b1.Property("CharacterId") + .HasColumnType("uuid"); + + b1.Property("FirstJob") + .HasColumnType("smallint"); + + b1.Property("FourthJob") + .HasColumnType("smallint"); + + b1.Property("SecondJob") + .HasColumnType("smallint"); + + b1.Property("ThirdJob") + .HasColumnType("smallint"); + + b1.HasKey("CharacterId"); + + b1.ToTable("Characters"); + + b1.WithOwner() + .HasForeignKey("CharacterId"); + }); + + b.Navigation("Account"); + + b.Navigation("BaseStats"); + + b.Navigation("Guild"); + + b.Navigation("JobData"); + }); + + modelBuilder.Entity("Server.DB.Documents.GuildMember", b => + { + b.HasOne("Server.DB.Documents.Character", "Character") + .WithMany() + .HasForeignKey("CharacterId"); + + b.HasOne("Server.DB.Documents.Guild", "Guild") + .WithMany("GuildMembers") + .HasForeignKey("GuildId"); + + b.Navigation("Character"); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("Server.DB.Documents.InventoryItem", b => + { + b.HasOne("Server.DB.Documents.Character", "Character") + .WithMany("InventoryItems") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("Server.DB.Documents.Account", b => + { + b.Navigation("Characters"); + }); + + modelBuilder.Entity("Server.DB.Documents.Character", b => + { + b.Navigation("InventoryItems"); + }); + + modelBuilder.Entity("Server.DB.Documents.Guild", b => + { + b.Navigation("GuildMembers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/DB/Migrations/20231116110504_DBPoolingAndLazyLoadingSupport.cs b/Server/DB/Migrations/20231116110504_DBPoolingAndLazyLoadingSupport.cs new file mode 100644 index 0000000..9b9cb8c --- /dev/null +++ b/Server/DB/Migrations/20231116110504_DBPoolingAndLazyLoadingSupport.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Server.DB.Migrations; + +/// +public partial class DBPoolingAndLazyLoadingSupport : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Name", + table: "Characters", + type: "varchar(20)", + maxLength: 20, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Characters_Id", + table: "Characters", + column: "Id", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Characters_Name", + table: "Characters", + column: "Name", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Characters_Id", + table: "Characters"); + + migrationBuilder.DropIndex( + name: "IX_Characters_Name", + table: "Characters"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Characters", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "varchar(20)", + oldMaxLength: 20, + oldNullable: true); + } +} diff --git a/Server/DB/Migrations/WonderkingContextModelSnapshot.cs b/Server/DB/Migrations/WonderkingContextModelSnapshot.cs index 28aea68..8e5308f 100644 --- a/Server/DB/Migrations/WonderkingContextModelSnapshot.cs +++ b/Server/DB/Migrations/WonderkingContextModelSnapshot.cs @@ -18,6 +18,9 @@ namespace Server.DB.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -41,6 +44,7 @@ namespace Server.DB.Migrations .HasColumnType("bytea"); b.Property("Username") + .HasMaxLength(20) .HasColumnType("varchar(20)"); b.HasKey("Id"); @@ -91,7 +95,8 @@ namespace Server.DB.Migrations .HasColumnType("integer"); b.Property("Name") - .HasColumnType("text"); + .HasMaxLength(20) + .HasColumnType("varchar(20)"); b.Property("PvPLevel") .HasColumnType("smallint"); @@ -102,6 +107,12 @@ namespace Server.DB.Migrations b.HasIndex("GuildId"); + b.HasIndex("Id") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + b.ToTable("Characters"); }); diff --git a/Server/DB/WonderkingContext.cs b/Server/DB/WonderkingContext.cs index 8ec7cf6..8251da3 100644 --- a/Server/DB/WonderkingContext.cs +++ b/Server/DB/WonderkingContext.cs @@ -1,27 +1,16 @@ +using JetBrains.Annotations; + namespace Server.DB; using Documents; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; public class WonderkingContext : DbContext { - private readonly IConfiguration _configuration; - private readonly ILoggerFactory _loggerFactory; - - public WonderkingContext(ILoggerFactory loggerFactory, IConfiguration configuration) + public WonderkingContext([NotNull] DbContextOptions options) : base(options) { - this._loggerFactory = loggerFactory; - this._configuration = configuration; } public DbSet Accounts { get; set; } public DbSet Characters { get; set; } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => - optionsBuilder - .UseNpgsql( - $"Host={this._configuration["DB:Host"]};Username={this._configuration["DB:Username"]};Password={this._configuration["DB:Password"]};Database={this._configuration["DB:Database"]};Port={this._configuration["DB:Port"]}") - .EnableSensitiveDataLogging().UseLoggerFactory(this._loggerFactory); } diff --git a/Server/Program.cs b/Server/Program.cs index a092fe9..3a3659d 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -17,10 +17,21 @@ builder.Configuration.AddJsonFile("settings.json", true, true) .AddJsonFile($"settings.{builder.Environment.EnvironmentName}.json", true) .AddEnvironmentVariables().Build(); builder.Services.AddLogging(); -builder.Logging.AddFile("Logs/Server-{Date}.log", LogLevel.Trace); -builder.Logging.AddFile("Logs/Server-{Date}.json.log", LogLevel.Trace, isJson: true); +var loggerFactory = LoggerFactory.Create(loggingBuilder => +{ + loggingBuilder.AddFile("Logs/Server-{Date}.log", LogLevel.Trace); + loggingBuilder.AddFile("Logs/Server-{Date}.json.log", LogLevel.Trace, isJson: true); + loggingBuilder.AddConsole(); +}); builder.Services.AddEntityFrameworkNpgsql(); -builder.Services.AddDbContext(); +builder.Services.AddDbContextPool(o => +{ + using var configuration = builder.Configuration; + o.UseNpgsql( + $"Host={configuration["DB:Host"]};Username={configuration["DB:Username"]};Password={configuration["DB:Password"]};Database={configuration["DB:Database"]};Port={configuration["DB:Port"]}") + .EnableSensitiveDataLogging().UseLazyLoadingProxies().UseLoggerFactory(loggerFactory); +}); +builder.Services.AddSingleton(loggerFactory); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); diff --git a/Server/Server.csproj b/Server/Server.csproj index 8342b6d..5745021 100644 --- a/Server/Server.csproj +++ b/Server/Server.csproj @@ -66,6 +66,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive