diff --git a/c#/crawler/src/Db/CrawlerDbContext.cs b/c#/crawler/src/Db/CrawlerDbContext.cs index 8abbbb71..8129f31b 100644 --- a/c#/crawler/src/Db/CrawlerDbContext.cs +++ b/c#/crawler/src/Db/CrawlerDbContext.cs @@ -77,26 +77,30 @@ protected override void OnModelCreating(ModelBuilder b) .HasOne(e => e.Content).WithOne().HasForeignKey(e => e.Spid); b.Entity().ToTable($"tbmc_f{Fid}_subReply_content"); - var thread = new RevisionWithSplitting.ModelBuilderExtension(b, "tbmcr_thread"); - thread.HasKey(e => new {e.Tid, e.TakenAt}); - thread.SplittingHasKey("viewCount", e => new {e.Tid, e.TakenAt}); + _ = new RevisionWithSplitting + .ModelBuilder(b, "tbmcr_thread", e => new {e.Tid, e.TakenAt}) + .HasBaseTable() + .SplitToTable("viewCount"); - var reply = new RevisionWithSplitting.ModelBuilderExtension(b, "tbmcr_reply"); - reply.HasKey(e => new {e.Pid, e.TakenAt}); - reply.SplittingHasKey("agreeCount", e => new {e.Pid, e.TakenAt}); - reply.SplittingHasKey("subReplyCount", e => new {e.Pid, e.TakenAt}); - reply.SplittingHasKey("floor", e => new {e.Pid, e.TakenAt}); + _ = new RevisionWithSplitting + .ModelBuilder(b, "tbmcr_reply", e => new {e.Pid, e.TakenAt}) + .HasBaseTable() + .SplitToTable("agreeCount") + .SplitToTable("subReplyCount") + .SplitToTable("floor"); - var subReply = new RevisionWithSplitting.ModelBuilderExtension(b, "tbmcr_subReply"); - subReply.HasKey(e => new {e.Spid, e.TakenAt}); - subReply.SplittingHasKey("agreeCount", e => new {e.Spid, e.TakenAt}); - subReply.SplittingHasKey("disagreeCount", e => new {e.Spid, e.TakenAt}); + _ = new RevisionWithSplitting + .ModelBuilder(b, "tbmcr_subReply", e => new {e.Spid, e.TakenAt}) + .HasBaseTable() + .SplitToTable("agreeCount") + .SplitToTable("disagreeCount"); - var user = new RevisionWithSplitting.ModelBuilderExtension(b, "tbmcr_user"); - user.HasKey(e => new {e.Uid, e.TakenAt}); - user.SplittingHasKey("ipGeolocation", e => new {e.Uid, e.TakenAt}); - user.SplittingHasKey("portraitUpdatedAt", e => new {e.Uid, e.TakenAt}); - user.SplittingHasKey("displayName", e => new {e.Uid, e.TakenAt}); + _ = new RevisionWithSplitting + .ModelBuilder(b, "tbmcr_user", e => new {e.Uid, e.TakenAt}) + .HasBaseTable() + .SplitToTable("ipGeolocation") + .SplitToTable("portraitUpdatedAt") + .SplitToTable("displayName"); b.Entity().Property(e => e.DisplayName).HasConversion(); b.Entity().Property(e => e.DisplayName).HasConversion(); diff --git a/c#/crawler/src/Db/Revision/Splitting/RevisionWithSplitting.cs b/c#/crawler/src/Db/Revision/Splitting/RevisionWithSplitting.cs index e52eed74..2892a30f 100644 --- a/c#/crawler/src/Db/Revision/Splitting/RevisionWithSplitting.cs +++ b/c#/crawler/src/Db/Revision/Splitting/RevisionWithSplitting.cs @@ -1,3 +1,5 @@ +using System.Collections.ObjectModel; + namespace tbm.Crawler.Db.Revision.Splitting; public abstract class RevisionWithSplitting : BaseRevisionWithSplitting @@ -23,16 +25,51 @@ protected void SetSplitEntityValue _splitEntities[typeof(TSplitEntity)] = entityFactory(); } - public class ModelBuilderExtension(ModelBuilder builder, string baseTableName) + public class ModelBuilder( + Microsoft.EntityFrameworkCore.ModelBuilder builder, + string baseTableName, + Expression> keySelector) { - public void HasKey(Expression> keySelector) - where TRevision : class, TBaseRevision => - builder.Entity().ToTable(baseTableName).HasKey(keySelector); - - public void SplittingHasKey - (string tableNameSuffix, Expression> keySelector) - where TRevisionWithSplitting : RevisionWithSplitting => - builder.Entity().Ignore(e => e.NullFieldsBitMask) - .ToTable($"{baseTableName}_{tableNameSuffix}").HasKey(keySelector); + public ModelBuilder HasBaseTable() where TRevision : TBaseRevision + { + var visitor = new ReplaceParameterTypeVisitor(); + _ = builder.Entity().ToTable(baseTableName) + .HasKey((Expression>) + visitor.Visit(keySelector)); + return this; + } + + public ModelBuilder SplitToTable(string tableNameSuffix) + where TRevisionWithSplitting : RevisionWithSplitting + { + var visitor = new ReplaceParameterTypeVisitor(); + _ = builder.Entity() + .Ignore(e => e.NullFieldsBitMask) + .ToTable($"{baseTableName}_{tableNameSuffix}") + .HasKey((Expression>) + visitor.Visit(keySelector)); + return this; + } + + /// https://stackoverflow.com/questions/38316519/replace-parameter-type-in-lambda-expression/38345590#38345590 + private sealed class ReplaceParameterTypeVisitor : ExpressionVisitor + { + private ReadOnlyCollection? _parameters; + + protected override Expression VisitParameter(ParameterExpression node) => + _parameters?.FirstOrDefault(p => p.Name == node.Name) ?? + (node.Type == typeof(TSource) ? Expression.Parameter(typeof(TTarget), node.Name) : node); + + protected override Expression VisitLambda(Expression node) + { + _parameters = VisitAndConvert(node.Parameters, nameof(VisitLambda)); + return Expression.Lambda(Visit(node.Body), _parameters); + } + + protected override Expression VisitMember(MemberExpression node) => + node.Member.DeclaringType == typeof(TSource) + ? Expression.Property(Visit(node.Expression)!, node.Member.Name) + : base.VisitMember(node); + } } }