Код элемента Entity Framework Сначала усечение десятичных знаков
Я использую Entity Framework 6.x, используя подход Code First в приложении MVC 5. В этой конкретной ситуации моя модель (среди прочего) содержит два свойства с именем Широта и долгота:
[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, который позволяет пользователю переместить маркер. Это хорошо работает. Когда маркер перемещается, поля Latitude и Longitude заполняются обновленным значением, которое (Javascript) имеет более 12 десятичных цифр. Это не имеет значения для AFAIK, потому что мой масштаб равен 8 десятичным знакам.
После нажатия кнопки отправки и вызова метода Create или Edit POST я исследую экземпляр модели, и я подтвердил, что фактические значения, переданные в модели контроллеру, верны, у них более десятичных цифр (эти это место Javascript-кода). Поэтому значение правильное.
Теперь... проблема заключается в том, что после выполнения db.SaveChanges() база данных обновляется - и я подтвердил, что фактическая запись/обновление произошла, но каким-то образом внутри EF игнорирует мои фактические значения и записывает усеченные широта/долгота округлена до ТОЛЬКО ДВА десятичных цифр, поэтому моя Локатор показывает в БД как 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 lon 11/8)
- Столбцы базы данных SQL имеют правильную точность/масштаб (лат 10/8, длинный 11/8)
- Значения, переданные в модели, имеют как минимум 8 десятичных цифр для широты и долготы
- фактическая запись/обновление значения происходит в базе данных без ошибок, но...
- Значения, записанные в базе данных для этих двух столбцов, усекаются до ДВА десятичных цифр и
покажите остальные наименьшие значащие десятичные цифры как ноль (0)
Ответы
Ответ 1
EF имеет специальное свойство для SqlProviderServices (реализация для поставщика SqlClient для SQL Server) - TruncateDecimalsToScale.
Значение по умолчанию равно true, поэтому, возможно, вы можете изменить его на значение false. Например:
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
Ответ 2
Для хранения пространственных данных я бы порекомендовал класс 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 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 это не влияет. Здесь округление по умолчанию.
РОС:
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);
}
}