简体   繁体   中英

EFCore EnsureCreated() run command after table creation

So I'm pretty new to EFCore (straight into 5.0 preview, because why not), and enjoying all the fluent configuration side of things. The whole IEntityTypeConfiguration<> concept really helps in making the Entities agnostic to the backing DB (as opposed to using attributes on the entities themselves).

I generate my whole database using DbContext.Database.EnsureCreated() in my Startup.cs . It's currently backed by SQLite while I'm playing around, but would like to eventually move to PostgreSQL with the TimescaleDB extension.

The thing is, when using TimescaleDB, I would need to issue a create_hypertable() immediately after creating the table itself.

What's the best way to issue a command after table creation?

  • Right after EnsureCreated() , using ExecuteSqlRaw() ? That means all the tables have been created before the SQL is executed. I would prefer having it as close to the table creation as possible.
  • Somewhere inside the DbContext ?
  • In my IEntityTypeConfiguration<> ?
  • or somewhere else?

Thanks!

If you want to keep using context.Database.EnsureCreated() , then running your own script (eg by executing context.Database.ExecuteSqlRaw() ) after the call is the way to go here.

That means all the tables have been created before the SQL is executed. I would prefer having it as close to the table creation as possible.

There shouldn't be any downsides to this as far as I can see, so you are not gaining anything by moving the create_hypertable() call closer to another table (if you disagree, please elaborate why this is the case).


If you really want to move the call near another command for some reason, than you could implement a DbCommandInterceptor . You could then parse the command text for a specific CREATE TABLE statement or something and then issue your own statement. (If you want to go that route, post a comment and I update this answer with some code.)


In case you switch from using context.Database.EnsureCreated() to migrations, you can use the Sql() method to inject your own statements:

public partial class MyMigration : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql("SELECT create_hypertable('conditions', 'time');")
    }
}

Maybe you can elaborate a bit, why you feel the need to move the create_hypertable() calls closer to their tables.


If you just care about having the create_hypertable() calls close in the C# code, but not necessarily in the generated SQL code, then introducing a custom attribute (or an interface) and a method to apply the hypertable to all entities that have properties decorated with this attribute (or implement this interface) might be good enough:

[AttributeUsage(AttributeTargets.Property)]
public class HypertableColumnAttribute : Attribute
{
}

public static class DbContextExtensions
{
    public static void ApplyHypertables(this Context context)
    {
        var entityTypes = context.Model.GetEntityTypes();

        foreach (var entityType in entityTypes)
        {
            foreach (var property in entityType.GetProperties())
            {
                if (property.ClrType.GetCustomAttribute(
                    typeof(HypertableColumnAttribute)) != null)
                {
                    var tableName = entityType.GetTableName();
                    var columnName = property.GetColumnName();

                    context.Database.ExecuteSqlInterpolated(
                        $"SELECT create_hypertable({tableName}, {columnName});");
                }
            }
        }
    }
}

public class IceCream
{
    public int IceCreamId { get; set; }
    public string Name { get; set; }

    [HypertableColumn]
    public DateTime? UpdatedAt { get; set; }
}

public class Context : DbContext
{
    public DbSet<IceCream> IceCreams { get; set; }

    /// ...
}

internal static class Program
{
    private static void Main()
    {
        using var context = new Context();

        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();
        context.ApplyHypertables();

        var iceCreams = context.IceCreams
            .OrderBy(i => i.IceCreamId)
            .ToList();
        
        Debug.Assert(iceCreams.Count == 2);
    }
}

In case you want to keep your model classes clean of any database specific attributes, you can use annotations instead. The idea is basically the same, but annotations are EF Core metadata and can be defined as part of an IEntityTypeConfiguration<T> implementation using the HasAnnotation() method:

public static class DbContextExtensions
{
    public static void ApplyHypertables(this Context context)
    {
        var entityTypes = context.Model.GetEntityTypes();

        foreach (var entityType in entityTypes)
        {
            foreach (var property in entityType.GetProperties())
            {
                var isHypertableColumn = property.FindAnnotation(MyAnnotationNames.Hypertable)?.Value;
                if ((bool)(isHypertableColumn ?? false))
                {
                    var tableName = entityType.GetTableName();
                    var columnName = property.GetColumnName();

                    context.Database.ExecuteSqlInterpolated(
                        $"SELECT create_hypertable({tableName}, {columnName});");
                }
            }
        }
    }
}

public static class MyAnnotationNames
{
    public const string Prefix = "MyPrefix:";
    public const string Hypertable = Prefix + "Hypertable";
}

public class IceCream
{
    public int IceCreamId { get; set; }
    public string Name { get; set; }
    public DateTime? UpdatedAt { get; set; }
}

public class IceCreamConfiguration : IEntityTypeConfiguration<IceCream>
{
    public void Configure(EntityTypeBuilder<IceCream> builder)
    {
        builder.Property(e => e.UpdatedAt)
            .HasAnnotation(MyAnnotationNames.Hypertable, true);
        
        builder.HasData(
            new IceCream {IceCreamId = 1, Name = "Vanilla"},
            new IceCream {IceCreamId = 2, Name = "Chocolate"});
    }
}

public class Context : DbContext
{
    public DbSet<IceCream> IceCreams { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
        => modelBuilder.ApplyConfiguration(new IceCreamConfiguration());

    /// ...
}

internal static class Program
{
    private static void Main()
    {
        using var context = new Context();

        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();
        context.ApplyHypertables();

        var iceCreams = context.IceCreams
            .OrderBy(i => i.IceCreamId)
            .ToList();
        
        Debug.Assert(iceCreams.Count == 2);
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM