osu/osu.Game/Database/OsuDbContext.cs
Bartłomiej Dach a16c0641b2 Revert EF Core to version 2.2
This reverts commit f3faad74d5, reversing
changes made to 712e7bc7bf.

Several issues arose after migrating to 5.0, including, but possibly not
limited to, performance regressions in song select, as well as failures
when attempting to save beatmaps after metadata changes in the editor.
2021-03-21 11:05:15 +01:00

215 lines
7.8 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using osu.Framework.Logging;
using osu.Framework.Statistics;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using DatabasedKeyBinding = osu.Game.Input.Bindings.DatabasedKeyBinding;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
using osu.Game.Skinning;
namespace osu.Game.Database
{
public class OsuDbContext : DbContext
{
public DbSet<BeatmapInfo> BeatmapInfo { get; set; }
public DbSet<BeatmapDifficulty> BeatmapDifficulty { get; set; }
public DbSet<BeatmapMetadata> BeatmapMetadata { get; set; }
public DbSet<BeatmapSetInfo> BeatmapSetInfo { get; set; }
public DbSet<DatabasedKeyBinding> DatabasedKeyBinding { get; set; }
public DbSet<DatabasedSetting> DatabasedSetting { get; set; }
public DbSet<FileInfo> FileInfo { get; set; }
public DbSet<RulesetInfo> RulesetInfo { get; set; }
public DbSet<SkinInfo> SkinInfo { get; set; }
public DbSet<ScoreInfo> ScoreInfo { get; set; }
private readonly string connectionString;
private static readonly Lazy<OsuDbLoggerFactory> logger = new Lazy<OsuDbLoggerFactory>(() => new OsuDbLoggerFactory());
private static readonly GlobalStatistic<int> contexts = GlobalStatistics.Get<int>("Database", "Contexts");
static OsuDbContext()
{
// required to initialise native SQLite libraries on some platforms.
SQLitePCL.Batteries_V2.Init();
// https://github.com/aspnet/EntityFrameworkCore/issues/9994#issuecomment-508588678
SQLitePCL.raw.sqlite3_config(2 /*SQLITE_CONFIG_MULTITHREAD*/);
}
/// <summary>
/// Create a new in-memory OsuDbContext instance.
/// </summary>
public OsuDbContext()
: this("DataSource=:memory:")
{
// required for tooling (see https://wildermuth.com/2017/07/06/Program-cs-in-ASP-NET-Core-2-0).
Migrate();
}
/// <summary>
/// Create a new OsuDbContext instance.
/// </summary>
/// <param name="connectionString">A valid SQLite connection string.</param>
public OsuDbContext(string connectionString)
{
this.connectionString = connectionString;
var connection = Database.GetDbConnection();
try
{
connection.Open();
using (var cmd = connection.CreateCommand())
{
cmd.CommandText = "PRAGMA journal_mode=WAL;";
cmd.ExecuteNonQuery();
}
}
catch
{
connection.Close();
throw;
}
contexts.Value++;
}
~OsuDbContext()
{
// DbContext does not contain a finalizer (https://github.com/aspnet/EntityFrameworkCore/issues/8872)
// This is used to clean up previous contexts when fresh contexts are exposed via DatabaseContextFactory
Dispose();
}
private bool isDisposed;
public override void Dispose()
{
if (isDisposed) return;
isDisposed = true;
base.Dispose();
contexts.Value--;
GC.SuppressFinalize(this);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder
// this is required for the time being due to the way we are querying in places like BeatmapStore.
// if we ever move to having consumers file their own .Includes, or get eager loading support, this could be re-enabled.
.ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.IncludeIgnoredWarning))
.UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10))
.UseLoggerFactory(logger.Value);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<BeatmapInfo>().HasIndex(b => b.OnlineBeatmapID).IsUnique();
modelBuilder.Entity<BeatmapInfo>().HasIndex(b => b.MD5Hash);
modelBuilder.Entity<BeatmapInfo>().HasIndex(b => b.Hash);
modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.OnlineBeatmapSetID).IsUnique();
modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.DeletePending);
modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.Hash).IsUnique();
modelBuilder.Entity<SkinInfo>().HasIndex(b => b.Hash).IsUnique();
modelBuilder.Entity<SkinInfo>().HasIndex(b => b.DeletePending);
modelBuilder.Entity<DatabasedKeyBinding>().HasIndex(b => new { b.RulesetID, b.Variant });
modelBuilder.Entity<DatabasedKeyBinding>().HasIndex(b => b.IntAction);
modelBuilder.Entity<DatabasedKeyBinding>().Ignore(b => b.KeyCombination);
modelBuilder.Entity<DatabasedKeyBinding>().Ignore(b => b.Action);
modelBuilder.Entity<DatabasedSetting>().HasIndex(b => new { b.RulesetID, b.Variant });
modelBuilder.Entity<FileInfo>().HasIndex(b => b.Hash).IsUnique();
modelBuilder.Entity<FileInfo>().HasIndex(b => b.ReferenceCount);
modelBuilder.Entity<RulesetInfo>().HasIndex(b => b.Available);
modelBuilder.Entity<RulesetInfo>().HasIndex(b => b.ShortName).IsUnique();
modelBuilder.Entity<BeatmapInfo>().HasOne(b => b.BaseDifficulty);
modelBuilder.Entity<ScoreInfo>().HasIndex(b => b.OnlineScoreID).IsUnique();
}
private class OsuDbLoggerFactory : ILoggerFactory
{
#region Disposal
public void Dispose()
{
}
#endregion
public ILogger CreateLogger(string categoryName) => new OsuDbLogger();
public void AddProvider(ILoggerProvider provider)
{
// no-op. called by tooling.
}
private class OsuDbLogger : ILogger
{
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (logLevel < LogLevel.Information)
return;
Framework.Logging.LogLevel frameworkLogLevel;
switch (logLevel)
{
default:
frameworkLogLevel = Framework.Logging.LogLevel.Debug;
break;
case LogLevel.Warning:
frameworkLogLevel = Framework.Logging.LogLevel.Important;
break;
case LogLevel.Error:
case LogLevel.Critical:
frameworkLogLevel = Framework.Logging.LogLevel.Error;
break;
}
Logger.Log(formatter(state, exception), LoggingTarget.Database, frameworkLogLevel);
}
public bool IsEnabled(LogLevel logLevel)
{
#if DEBUG_DATABASE
return logLevel > LogLevel.Debug;
#else
return logLevel > LogLevel.Information;
#endif
}
public IDisposable BeginScope<TState>(TState state) => null;
}
}
public void Migrate() => Database.Migrate();
}
}