Entity Framework Code Първо съкращавам моите десетични знаци

Използвам Entity Framework 6.x, като използвам подхода Code First на приложение MVC 5. В тази конкретна ситуация моят модел (наред с други неща) съдържа две свойства, наречени Latitude и Longitude:

[Required, Range(-90, +90)]
public decimal Latitude { get; set; }

[Required, Range(-180, +180)]
public decimal Longitude { get; set; }

И когато извърших миграцията, получих нещо подобно

CreateTable("ResProperty"), c => new {
        :
    Latitude = c.Decimal(nullable: false, precision: 10, scale: 8),
    Longitude = c.Decimal(nullable: false, precision: 11, scale: 8),
        :
})
... other stuff

така че и географската ширина, и дължината имат 8 десетични цифри. Първият с 2 цели числа (макс. 90), а вторият с 3 цели числа (макс. 180).

След изпълнение на командата Update-Database колоните на моята таблица се показват като:

Latitude decimal(10,8)
Longitude decimal(11,8)

това ми се струва добре. Сега според мен имам карта и Javascript код, който позволява на потребителя да препозиционира маркера. Това също работи добре. Когато маркерът е преместен, полетата за географска ширина и дължина се попълват с актуализираната стойност, която (Javascript) има повече от 12 десетични цифри. Това няма значение AFAIK, защото моята скала е 8 знака след десетичната запетая.

След като се натисне бутонът за изпращане и се извика методът Създаване или Редактиране на POST, разглеждам екземпляра на модела и потвърдих, че действителните стойности, предадени в модела към контролера, са правилни, те имат повече от достатъчно десетични цифри (тези, които Javascript код място). Значи стойността е правилна.

Сега... проблемът е, че след изпълнението на db.SaveChanges() базата данни се актуализира - и аз потвърдих, че е извършено действително запис/актуализация - но по някакъв начин вътрешно EF пренебрегва действителните ми стойности и записва съкратена ширина/дължина закръглена САМО до ДВЕ десетични цифри, така че моят Latitude се показва в DB като 09.500000000, всички други десетични цифри са нулирани, защото изглежда, че е имало закръгляване.

// Prior to SaveChanges()
Latitude = 9.08521879
Longitude = -79.51658792
// After SaveChanges()
Latitude = 9.08000000
Longitude = -79.51000000

Защо се закръгля, ако съм дал правилния мащаб и точност и колоната също има правилния мащаб и точност? защо SaveChanges променя моите стойности?

Намерих тази публикация (http://weiding331.blogspot.com/2014/01/entity-framework-decimal-value.html), което е същият проблем, но не знам как мога да го поправя (ако го направи), защото вече извърших няколко миграции и добавяне на данни след въпросната таблица беше "мигрирана".

Обобщаване

  • Типът данни на модела е правилен (десетичен)
  • Кодът за миграция на базата данни има правилната прецизия/мащаб (ширина 10/8 и дължина 11/8)
  • Колоните на SQL базата данни имат правилната прецизност/мащаб (широта 10/8, дължина 11/8)
  • Стойностите, предадени в модела, имат поне 8 десетични цифри за географска ширина и дължина
  • действителното записване/актуализиране на стойността се извършва в базата данни без грешка, но...
  • Стойностите, записани в базата данни за тези две колони, се съкращават до ДВЕ десетични цифри и показват другите най-малко значими десетични цифри като нула (0)

person Lord of Scripts    schedule 15.10.2014    source източник
comment
Посочихте ли мащаб и прецизност във вашия модел? Разгледайте този въпрос stackoverflow.com/questions/3504660/? От друга страна, EF поддържа типове пространствени данни...   -  person Pawel    schedule 16.10.2014
comment
Не мисля, че мога да използвам OnModelCreating повече, защото тази конкретна миграция беше няколко нива надолу в списъка ми за миграция. Добавих го post mortem и стартирах Update-Database (без нова миграция), но не видях да решава проблема. Освен това SQL колоните в DB показаха правилната точност, но EF я съкращаваше вътрешно по време на SaveChanges().   -  person Lord of Scripts    schedule 17.10.2014


Отговори (2)


EF има специално свойство за SqlProviderServices (имплементация за доставчика на SqlClient за SQL Server) - TruncateDecimalsToScale. Стойността по подразбиране е вярно, така че може би можете да я промените на фалшива стойност. Например:

public class DbContextConfiguration : DbConfiguration
    {
        public DbContextConfiguration()
        {
            var now = SqlProviderServices.Instance;
            SqlProviderServices.TruncateDecimalsToScale = false;
            this.SetProviderServices(SqlProviderServices.ProviderInvariantName, SqlProviderServices.Instance);
        }
    }

    [DbConfigurationType(typeof(DbContextConfiguration))]
    public class MyContext : DbContext
    { ... }

Повече информация за това: https://msdn.microsoft.com/en-us/library/system.data.entity.sqlserver.sqlproviderservices.truncatedecimalstoscale%28v=vs.113%29.aspx

person Adrian Tarnowski    schedule 23.08.2016
comment
Това изглежда точно това, което търся, но бихте ли обяснили как се задава стойността тук? Сега има променлива, която изглежда не се използва и свойството изглежда е зададено на статичен метод. Това ли е най-добрият начин за задаване на тази стойност? Изненадан съм, че такава важна функция няма повече документация около нея - съкращаването на десетични знаци със сигурност е огромен проблем. Благодаря. - person Mark007; 08.10.2016
comment
@Mark007 това е глобална конфигурация. Разбира се също не е добро място за настройка на тези неща, но знаете, че е EF;) - person Adrian Tarnowski; 21.03.2017
comment
Работи и реши проблема, но мисля, че трябва да шпионирам нещата и да видя точно какво се случва в даден момент. - person Mark007; 23.03.2017
comment
Счита ли се това за грешка? Не виждам как не е... все пак мащабирането е правилно, но все още се съкращава неподходящо? - person Lopsided; 17.11.2017

Съхраняване на пространствени данни Бих препоръчал DbGeography клас, който е създаден за този тип данни.

https://docs.microsoft.com/en-us/dotnet/api/system.data.entity.spatial.dbgeography?view=entity-framework-6.2.0


Както вече споменахме, проблемът с отрязването може да бъде решен с SqlProviderServices.TruncateDecimalsToScale = false;, както посочи @AdrianTarnowski. Бих искал обаче да покажа защо това се случва и защо Entity Framework 6.X съкращава десетичните стойности, вместо да ги закръгля по подразбиране.

За да тествам, използвам основна програма като тази:

class Program
{
    static void Main(string[] args)
    {
        var dbContext = new ApplicationDbContext();
        dbContext.TestValues.Add(new TestValue()
        {
            Value = 0.0005m
        });
        dbContext.TestValues.Add(new TestValue()
        {
            Value = 0.0001m
        });
        dbContext.TestValues.Add(new TestValue()
        {
            Value = 0.0007m
        });
        dbContext.SaveChanges();
    }
}

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

    public decimal Value { get; set; }
}

public class DbContextConfiguration : DbConfiguration
{
    public DbContextConfiguration()
    {
        var providerInstance = SqlProviderServices.Instance;
        SqlProviderServices.TruncateDecimalsToScale = true;
        this.SetProviderServices(SqlProviderServices.ProviderInvariantName, SqlProviderServices.Instance);
    }
}

[DbConfigurationType(typeof(DbContextConfiguration))]
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext() : base("ApplicationContext")
    {
        Database.Log = s => Debug.WriteLine(s);
    }

    public DbSet<TestValue> TestValues { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<TestValue>().Property(x => x.Value).HasPrecision(18, 3);

        base.OnModelCreating(modelBuilder);
    }
}

По подразбиране изглежда така: SqlProviderServices.TruncateDecimalsToScale = true;. Това е за предотвратяване на счупване на съществуващи приложения, които зависят от това поведение.

https://docs.microsoft.com/en-us/dotnet/api/system.data.entity.sqlserver.sqlproviderservices.truncatedecimalstoscale?redirectedfrom=MSDN&view=entity-framework-6.2.0#overloads

Когато TruncateDecimalsToScale е нормален (TruncateDecimalsToScale = true;), вмъкване от рамка на обект изглежда така в Database.Log от DbContext:

INSERT [dbo].[TestValues]([Value])
VALUES (@0)
SELECT [Id]
FROM [dbo].[TestValues]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()


-- @0: '0,0005' (Type = Decimal, Precision = 18, Scale = 3)

Въпреки това, гледайки SQL Server Profiler, действителните данни, които се изпращат, са 0 за всяка стойност от по-горе.

exec sp_executesql N'INSERT [dbo].[TestValues]([Value])
VALUES (@0)
SELECT [Id]
FROM [dbo].[TestValues]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()',N'@0 decimal(18,3)',@0=0

Вместо това промяната на SqlProviderServices.TruncateDecimalsToScale = false; на Database.Log от DbContext изглежда така:

INSERT [dbo].[TestValues]([Value])
VALUES (@0)
SELECT [Id]
FROM [dbo].[TestValues]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()


-- @0: '0,0005' (Type = Decimal)

Сега SQL Server Profiler изглежда по-добре и има правилни стойности:

exec sp_executesql N'INSERT [dbo].[TestValues]([Value])
VALUES (@0)
SELECT [Id]
FROM [dbo].[TestValues]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()',N'@0 decimal(4,4)',@0=5

Имайте предвид, че EntityFrameworkCore не е засегнат от това. Тук закръгляването е по подразбиране.

POC:

class Program
{
    static void Main(string[] args)
    {
        using (var dbContext = new ApplicationDbContext())
        {
            dbContext.TestValues.Add(new TestValue()
            {
                Value = 0.0005m
            });
            dbContext.TestValues.Add(new TestValue()
            {
                Value = 0.0001m
            });
            dbContext.TestValues.Add(new TestValue()
            {
                Value = 0.0007m
            });
            dbContext.SaveChanges();
        }
    }
}

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

    public decimal Value { get; set; }
}

public class ApplicationDbContext : DbContext
{
    public DbSet<TestValue> TestValues { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer("data source=localhost;initial catalog=;persist security info=True;User Id=;Password=;", providerOptions => providerOptions.CommandTimeout(60));
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<TestValue>().Property(x => x.Value).HasColumnType("decimal(18, 3)");

        base.OnModelCreating(modelBuilder);
    }
}
person Ogglas    schedule 18.07.2019