Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
361 views
in Technique[技术] by (71.8m points)

ef code first - Entity Framework 6: How to override SQL generator?

I'd like to amend the SQL that's being generated by EF:CF when generating the database schema (DDL), as suggested by the Entity Framework team.

How can this be done?

I couldn't find anything appropriate via Google.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

You can override the MigrationSqlGenerator that is used by Entity Framework by calling the DbMigrationsConfiguration.SetSqlGenerator() method in the constructor of your DbMigrationsConfiguration class, passing the database provider name (e.g. "System.Data.SqlClient" for SQL Server), and the MigrationSqlGenerator instance to use for that database provider.

Consider the example from the work item that you linked to:

public class MyEntity
{
    public int Id { get; set; }

    [Required]
    [MinLength(5)]
    public string Name { get; set; }
}

Suppose that the table for MyEntity had already been generated and the Add-Migration command was used to add the Name field.

By default, the scaffolded migration is:

public partial class AddMyEntity_Name : DbMigration
{
    public override void Up()
    {
        AddColumn("dbo.MyEntity", "Name", c => c.String(nullable: false));
    }

    public override void Down()
    {
        DropColumn("dbo.MyEntity", "Name");
    }
}

Notice that the scaffolder did not generate anything for the MinLengthAttribute.

To have EF convey the minimum length requirement, you can specify an attribute-to-column annotation convention. As mentioned on that documentation page, any AnnotationValues are ignored by the default SQL generators.

Within your DbContext's OnModelCreating() override, add the following:

modelBuilder.Conventions.Add(new AttributeToColumnAnnotationConvention<MinLengthAttribute, Int32>("minLength", (property, attributes) => attributes.Single().Length));

After adding that, you can regenerate the scaffolded migration by running Add-Migration -Force AddMyEntity_Name. Now the scaffolded migration is:

public partial class AddMyEntity_Name : DbMigration
{
    public override void Up()
    {
        AddColumn("dbo.MyEntity", "Name", c => c.String(nullable: false,
            annotations: new Dictionary<string, AnnotationValues>
            {
                { 
                    "minLength",
                    new AnnotationValues(oldValue: null, newValue: "5")
                },
            }));
    }

    public override void Down()
    {
        DropColumn("dbo.MyEntity", "Name",
            removedAnnotations: new Dictionary<string, object>
            {
                { "minLength", "5" },
            });
    }
}

Suppose that, as in the linked work item, you want to generate a constraint to check that the trimmed Name value is greater than the minLength (5 in this case).

You can start by creating a custom MigrationSqlGenerator that extends SqlServerMigrationSqlGenerator and call SetSqlGenerator() to install the custom MigrationSqlGenerator:

internal class CustomSqlServerMigrationSqlGenerator : SqlServerMigrationSqlGenerator
{
    protected override void Generate(AddColumnOperation addColumnOperation)
    {
        base.Generate(addColumnOperation);
    }
}

internal sealed class Configuration : DbMigrationsConfiguration<DataContext>
{
    public Configuration()
    {
        AutomaticMigrationsEnabled = false;

        SetSqlGenerator("System.Data.SqlClient", new CustomSqlServerMigrationSqlGenerator());
    }

    protected override void Seed(DataContext context)
    {
        //...
    }
}

Right now, this CustomSqlServerMigrationSqlGenerator overrides the Generate(AddColumnOperation) method, but simply calls the base implementation.

If you look at the documentation of AddColumnOperation, you will see two important properties, Column and Table. Column is the ColumnModel that was created by the lambda in Up(), c => c.String(nullable: false, annotations: ...).

In the Generate() method, you can access the custom AnnotationValues via the Annotations property of the ColumnModel.

To generate the DDL that adds the constraint, you need to generate the SQL and call the Statement() method. For example:

internal class CustomSqlServerMigrationSqlGenerator : SqlServerMigrationSqlGenerator
{
    protected override void Generate(AddColumnOperation addColumnOperation)
    {
        base.Generate(addColumnOperation);

        var column = addColumnOperation.Column;
        if (column.Type == System.Data.Entity.Core.Metadata.Edm.PrimitiveTypeKind.String)
        {
            var annotations = column.Annotations;
            AnnotationValues minLengthValues;
            if (annotations.TryGetValue("minLength", out minLengthValues))
            {
                var minLength = Convert.ToInt32(minLengthValues.NewValue);
                if (minLength > 0)
                {
                    if (Convert.ToString(column.DefaultValue).Trim().Length < minLength)
                    {
                        throw new ArgumentException(String.Format("minLength {0} specified for {1}.{2}, but the default value, '{3}', does not satisfy this requirement.", minLength, addColumnOperation.Table, column.Name, column.DefaultValue));
                    }

                    using (var writer = new StringWriter())
                    {
                        writer.Write("ALTER TABLE ");
                        writer.Write(Name(addColumnOperation.Table));
                        writer.Write(" ADD CONSTRAINT ");
                        writer.Write(Quote("ML_" + addColumnOperation.Table + "_" + column.Name));
                        writer.Write(" CHECK (LEN(LTRIM(RTRIM({0}))) > {1})", Quote(column.Name), minLength);
                        Statement(writer.ToString());
                    }
                }
            }
        }
    }
}

If you run Update-Database -Verbose, you will see an exception generated by CustomSqlServerMigrationSqlGenerator:

minLength 5 specified for dbo.MyEntity.Name, but the default value, '', does not satisfy this requirement.

To fix this issue, specify a defaultValue in the Up() method that is longer than the minimum length (e.g. "unknown"):

    public override void Up()
    {
        AddColumn("dbo.MyEntity", "Name", c => c.String(nullable: false, defaultValue: "unknown",
            annotations: new Dictionary<string, AnnotationValues>
            {
                { 
                    "minLength",
                    new AnnotationValues(oldValue: null, newValue: "5")
                },
            }));
    }

Now if you re-run Update-Database -Verbose, you will see the ALTER TABLE statement that adds the column and the ALTER TABLE statement that adds the constraint:

ALTER TABLE [dbo].[MyEntity] ADD [Name] [nvarchar](max) NOT NULL DEFAULT 'unknown'
ALTER TABLE [dbo].[MyEntity] ADD CONSTRAINT [ML_dbo.MyEntity_Name] CHECK (LEN(LTRIM(RTRIM([Name]))) > 5)

See also: EF6: Writing Your Own Code First Migration Operations, which shows how to implement a custom migration operation.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...