Вставка .Net / SQL и выбор в datetime2 не совпадают

#c# #sql-server #.net-core #azure-sql-database

#c# #sql-сервер #.net-core #azure-sql-database

Вопрос:

У меня есть фрагмент кода, который вставляет образец данных ([id datetime2] …) в базу данных SQLAzure.

Позже для каждого вызова я делаю select, чтобы узнать, есть ли PrimaryKey уже в DB [id datetime2], поэтому я обновляю его, в противном случае я делаю insert. Проблема в том, что select ничего не возвращает, но при вставке будет получена ошибка с дублированным ключом. (!?)

Я создал образец кода, воспроизводящий мою проблему, не каждый раз, но большую ее часть.

Если я заменю

 command.Parameters.AddWithValue("@Date", date);
 

Автор:

 command.Parameters.Add("@Date", SqlDbType.DateTime2).Value = date;
 

Это будет работать, но я хотел бы понять, почему команды select и insert не совпадают.

 //CODE - BEGIN
//.NetCore 2.2
//SqlDatabase Azure

var date = DateTime.Now;

command.Parameters.AddWithValue("@Id", 1); 
command.Parameters.AddWithValue("@Date", Date); //{19:33:22.7727095}
command.CommandText = "INSERT INTO Answer (Id , Date) VALUES (@Id, @Date)";
command.ExecuteNonQuery();

/*DB
**IdDevice  Date
**1 2019-04-18 19:33:22.7733333
*/

//Retry
command.CommandText = "SELECT TOP 1 Id FROM Answer where Id = @Id AND Date = @Date;";
var exist = command.ExecuteScalar();
if (exist == null)
{
    throw;
}
//CODE - END
 

Это всего лишь краткий пример для воспроизведения поведения, я использую точно такой же параметр для insert select, но select ничего не вернет.
Возможно, .net Datetime преобразуется в SqlDateTime для вставки и в SqlDateTime2 для выбора…

Комментарии:

1. ваши данные в столбце Id вставляют -1 в БД, и его право выбора не работает! правильно ли это указано в идентификаторе столбца в хранилище БД -1?

2. команда. Параметры. AddWithValue(«@Date», значение. Дата); //{19:33:22.7727095} в этой строке что такое ценность?

3. Не используйте AddWithValue . Создайте параметр с определенным типом, точностью и задайте его значение.

4. @hassan.ef мой плохой, его ‘1’ в БД, проблема, если локализована на дату 😉

5. @AmitYadav опять моя ошибка, ошибка между реальным кодом и образцом кода, значение равно DateTime.Now

Ответ №1:

AddWithValue выводит SqlDbType из предоставленного типа .NET как DateTime , а не DateTime2 . Затем доли секунды усекаются с точностью до 3 и округляются до 1/300 секунды, чтобы соответствовать менее точному типу данных параметра. Вы увидите это менее точное значение, если вы запрашиваете базу данных без WHERE предложения.

Усечение / округление не происходит с explict SqlDbType.DateTime2 , поскольку .NET DateTime и SqlDbType.DateTime2 оба поддерживают дробные секунды с точностью до 7.

Это еще одна причина, которой следует избегать AddWithValue .

Смешивание типов datetime / datetime2 также может привести к неожиданному поведению, как показано в вашем SELECT запросе. datetime2 имеет более высокий приоритет типа данных, чем datetime, поэтому значение доли секунды сравнивается с использованием datetime фактического значения 1/300 секунды, увеличенного до большей точности, а не округленного / усеченного. Рассмотрим эти примеры T-SQL:

 --these values compare not equal because the datetime value of 1/300 is actually .003333333333...
DECLARE @datetime datetime =   '2019-04-19T00:00:00.003';
DECLARE @datetime2 datetime2 = '2019-04-19T00:00:00.003';
IF @datetime = @datetime2 PRINT 'EQUAL' ELSE PRINT 'NOT EQUAL';
GO

--these values compare not equal because the datetime value is actually .006666666666...
DECLARE @datetime datetime =   '2019-04-19T00:00:00.007';
DECLARE @datetime2 datetime2 = '2019-04-19T00:00:00.007';
IF @datetime = @datetime2 PRINT 'EQUAL' ELSE PRINT 'NOT EQUAL';
GO

--these values comare equal because the datetime value is .010000000000...
DECLARE @datetime datetime =   '2019-04-19T00:00:00.010';
DECLARE @datetime2 datetime2 = '2019-04-19T00:00:00.010';
IF @datetime = @datetime2 PRINT 'EQUAL' ELSE PRINT 'NOT EQUAL';
GO
 

Хотя это изменение, нарушающее поведение сравнения, можно контролировать, запустив на уровне совместимости с базой данных 120 или ниже, было бы лучше просто сопоставить типы SQL. Это обеспечит наилучшую производительность и надежность вашего кода в будущем.

Редактировать:

То же самое поведение можно продемонстрировать с параметрами .NET несовпадающих типов. Ниже приведен пример PowerShell.

 $connectionString = "Data Source=.;Initial Catalog=tempdb;Integrated Security=SSPI"
$connection = New-Object System.Data.SqlClient.SqlConnection($connectionString)
$connection.Open()
$command = New-Object System.Data.SqlClient.SqlCommand("CREATE TABLE dbo.Answer (Id int NOT NULL, Date datetime2 NOT NULL);", $connection)
[void]$command.ExecuteNonQuery()

$command.CommandText = "INSERT INTO dbo.Answer (Id, Date) VALUES (@Id, @Date);"
[void]$command.Parameters.AddWithValue("@Id", 1)
[void]$command.Parameters.AddWithValue("@Date", [DateTime]::Parse("2019-04-19T00:00:00.003"))
[void]$command.ExecuteNonQuery()
$command.CommandText = "SELECT TOP 1 Id FROM Answer where Id = @Id AND Date = @Date;"
$exists = $command.ExecuteScalar()
# not exists
if($exists -ne $null) { Write-Host "exists" } else { Write-Host "not exists" }

$command.CommandText = "INSERT INTO dbo.Answer (Id, Date) VALUES (@Id, @Date);"
$command.Parameters["@Id"].Value = 2
$command.Parameters["@Date"].Value = [DateTime]::Parse("2019-04-19T00:00:00.007")
[void]$command.ExecuteNonQuery()
$command.CommandText = "SELECT TOP 1 Id FROM Answer where Id = @Id AND Date = @Date;"
$exists = $command.ExecuteScalar()
# not exists
if($exists -ne $null) { Write-Host "exists" } else { Write-Host "not exists" }

$command.CommandText = "INSERT INTO dbo.Answer (Id, Date) VALUES (@Id, @Date);"
$command.Parameters["@Id"].Value = 3
$command.Parameters["@Date"].Value = [DateTime]::Parse("2019-04-19T00:00:00.010")
[void]$command.ExecuteNonQuery()
$command.CommandText = "SELECT TOP 1 Id FROM Answer where Id = @Id AND Date = @Date;"
$exists = $command.ExecuteScalar()
# exists
if($exists -ne $null) { Write-Host "exists" } else { Write-Host "not exists" }    

$connection.Close()
 

Комментарии:

1. Конечно, как я уже сказал, если я использую command. Parameters.Add(«@Date», SqlDbType.DateTime2).Value = date; Работает нормально, но я не понимаю, как использование одного и того же Sql.Parameter (добавленного с помощью AddParamerWithValue ) может дать другой результат, используемый в команде выбора и вставки…

2. @CarlosBarroso, я добавлю к своему ответу, чтобы объяснить, почему неожиданные результаты с тем же значением параметра.

3. Я прекрасно понимаю ваш пример, но на самом деле это не мой вариант использования. Чтобы возобновить command.Parameters.AddWithValue("@Date", Datetime.Now); command.CommandText = "INSERT INTO Answer (Date) VALUES (@Date)"; command.ExecuteNonQuery(); command.CommandText = "SELECT TOP 1 Id FROM Answer where Date = @Date;"; command.ExecuteScalar(); //return null ту же базу данных, ту же таблицу, тот же тип, тот же параметр, разное поведение между выбором и вставкой.

4. @CarlosBarroso, ваш случай точно такой же, как в примере с T-SQL, но с WHERE предложением вместо IF оператора. Я добавил пример .NET, чтобы проиллюстрировать, что поведение сравнения такое же. Вы упомянули тот же тип, но тип отличается (SqlDbType.DateTime вместо SqlDbType.DateTime2).

5. В вашем TSQL вы сравниваете SqlDateTime с sqlDatetime2, поэтому ожидается сбой, ничего общего с моим поведением. Но ваш пример .net отлично воспроизводит поведение, я все еще не понимаю, почему при вставке будет использоваться SqlDateTime2, а в Where используется округленное SqlDateTime, это не имеет смысла 🙂