JPA entityManager.merge преобразует LocalDateTime в SQLServer 2012 DATETIME2 - PullRequest
0 голосов
/ 06 декабря 2018

У меня есть таблица со столбцом DateTime в качестве первичного ключа:

USE [idatest]
GO

CREATE TABLE [dbo].[DatesTbl](
    [creationDate] [datetime] NOT NULL
 CONSTRAINT [PK_DatesTbl] PRIMARY KEY CLUSTERED
(
    [creationDate] ASC
))
GO

Когда я делаю entityManager.merge, я получаю дубликат, нарушение PK, так как datetime содержит 3 цифры для milisec, но hibernet преобразует егодо datetime2, который содержит 7 цифр для milisec.В коде java я использую LocaDatetime, который содержит 10 цифр в миллисекундах.

Я пробовал решение, описанное в Отображение Himenate MSSQL datetime2 , но оно не работает: код Java выглядит следующим образом: pom.xml

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.3.RELEASE</version>
</parent>

<groupId>com.example</groupId>
<artifactId>spring-jap-test</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</build>

<dependencies>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
    </dependency>
    <dependency>
        <groupId>com.microsoft.sqlserver</groupId>
        <artifactId>mssql-jdbc</artifactId>
        <version>7.0.0.jre8</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

Класс DatesTbl

@Entity
@NoArgsConstructor
@AllArgsConstructor
public class DatesTbl {

    @Column(columnDefinition = "DATETIME", nullable = false)
    @Id
    private LocalDateTime creationDate;
}

Основной класс

@EnableTransactionManagement
public class Main {

    public static void main(String[] args) {

        ApplicationContext context = new AnnotationConfigApplicationContext(Main.class);

        EntityManagerFactory entityManagerFactory = context.getBean(EntityManagerFactory.class);
        final EntityManager entityManager = entityManagerFactory.createEntityManager();
        final LocalDateTime creationDate = LocalDateTime.of(2018, 12, 26, 8, 10, 40, 340);
        entityManager.getTransaction().begin();
        final DatesTbl datesTbl = entityManager.merge(new DatesTbl(creationDate));
        entityManager.getTransaction().commit();

        System.out.println("test");
    }

    @Bean
    @Primary
    public DataSource getDataSource() {

        SQLServerDataSource ds = null;
        try {
            ds = new SQLServerDataSource();
            ds.setServerName("localhost");
            ds.setDatabaseName("idatest");
            ds.setIntegratedSecurity(true);
        } catch (Exception ex) {
            System.out.println(ex.getMessage());
        }
        return ds;
    }


    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(true);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        hibernateJpaVendorAdapter.setDatabase(Database.SQL_SERVER);
        return hibernateJpaVendorAdapter;
    }


    @Bean
    public LocalContainerEntityManagerFactoryBean abstractEntityManagerFactoryBean(
            JpaVendorAdapter jpaVendorAdapter) {

        Properties properties = new Properties();
         properties.setProperty(FORMAT_SQL, String.valueOf(true));
        properties.setProperty(SHOW_SQL, String.valueOf(true));
        properties.setProperty(DIALECT, ModifiedSQLServerDialect.class.getTypeName());
        LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean =
                new LocalContainerEntityManagerFactoryBean();

        localContainerEntityManagerFactoryBean.setDataSource(getDataSource());
        localContainerEntityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter);
        localContainerEntityManagerFactoryBean.setJpaProperties(properties);
        localContainerEntityManagerFactoryBean.setPackagesToScan("enteties");

        return localContainerEntityManagerFactoryBean;
    }


    @Bean
    public PlatformTransactionManager platformTransactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}


public class ModifiedSQLServerDialect extends SQLServer2012Dialect {


    public ModifiedSQLServerDialect () {
        super();
        registerColumnType(Types.TIMESTAMP, "timestamp");
        registerColumnType(Types.DATE, "timestamp");
        registerColumnType(Types.TIME, "timestamp");
        registerHibernateType(Types.TIMESTAMP, "timestamp");
        registerHibernateType(Types.DATE, "timestamp");
        registerHibernateType(Types.TIME, "timestamp");
    }
}

, но все равно я вижу в профилировщике SQLServer:

exec sp_executesql N'select datestbl0_.creationDate as creation1_0_0_ from DatesTbl datestbl0_ where datestbl0_.creationDate=@P0        ',N'@P0 `datetime2`','2018-12-26 08:10:40.0000003'

Что не так с решением?

Ответы [ 2 ]

0 голосов
/ 11 декабря 2018

Объяснение

Проблема связана с проблемой в mssql-jdbc (версии 4.x и 6.x), PreparedStatement.setTimestamp (index, timestamp, calendar) имеет проблемы с преобразованием типов данных , который всегда отправляет параметр LocalDateTime с типом данных datetime2 на сервер SQL (игнорируя тип столбца таблицы).Из-за различий в точности datetime (0,00333сек) и datetime2 (100 наносекунд) и datetime используется в качестве PK, Hibernate в этом случае работает неправильно.

Когда мы запускаем основную программу, creationDate имеет значение 2018-12-26 08: 10: 40.000000340 , и значение сохраняется как 2018-12-2608: 10: 40.000 в БД, так как Hibernate не находит записи с тем же ключом в БД.Когда мы снова запустим основную программу, Hibernate сначала проверяет, есть ли какая-либо запись с тем же ключом, используя

', выбирая datestbl0_.creationDate в качестве creation1_0_0_ из DatesTbl datestbl0_, где datestbl0_.creationDate=@P0', N'@ P0' datetime2 '', '2018-12-26 08: 10: 40.0000003'

Похоже, что SQL Server вывел значение datetime в таблице до datetime2 для сравнения изапись не возвращается.Следовательно, Hibernate вставляет запись снова и приводит к нарушению первичного ключа.

Обходной путь

Как предположил Влад Михалча, не рекомендуется использовать столбец DATETIME в качестве PK.
Однако предположим, что нам все еще нужен столбец datetime в качестве PK, следующий обходной путь должен работать.Ключ к решению этой проблемы состоит в том, чтобы сравнение между datetime и datetime2 вернуло true.Чтобы достичь этого, мы можем обрезать / округлить значение datetime2 до соответствующего значения datetime перед передачей в БД.Следующие изменения в основной программе протестированы с SQL Server 2012 Express без ошибок.

public static void main(String[] args) {
    ApplicationContext context = new AnnotationConfigApplicationContext(Main.class);

    EntityManagerFactory entityManagerFactory = context.getBean(EntityManagerFactory.class);
    final EntityManager entityManager = entityManagerFactory.createEntityManager();

    LocalDateTime creationDate0 = LocalDateTime.of(2018, 12, 26, 8, 10, 40, 341340340);
    LocalDateTime creationDate3 = LocalDateTime.of(2018, 12, 26, 8, 10, 40, 343340340);
    LocalDateTime creationDate7 = LocalDateTime.of(2018, 12, 26, 8, 10, 40, 346670340);
    LocalDateTime creationDate10 = LocalDateTime.of(2018, 12, 26, 8, 10, 40, 349670340);
    entityManager.getTransaction().begin();
    final DatesTbl datesTbl0 = entityManager.merge(new DatesTbl(roundNanoSecForDateTime(creationDate0)));
    final DatesTbl datesTbl3 = entityManager.merge(new DatesTbl(roundNanoSecForDateTime(creationDate3)));
    final DatesTbl datesTbl7 = entityManager.merge(new DatesTbl(roundNanoSecForDateTime(creationDate7)));
    final DatesTbl datesTbl10 = entityManager.merge(new DatesTbl(roundNanoSecForDateTime(creationDate10)));
    entityManager.getTransaction().commit();
    System.out.println("test");
}

private static LocalDateTime roundNanoSecForDateTime(LocalDateTime localDateTime) {
    int nanoSec = localDateTime.getNano();
    // The rounding is based on following results on SQL server 2012 express
    // select cast(cast('2018-12-26 08:10:40.3414999' as datetime2) as datetime);
    // 2018-12-26 08:10:40.340
    // select cast(cast('2018-12-26 08:10:40.3415000' as datetime2) as datetime);
    // select cast(cast('2018-12-26 08:10:40.3444999' as datetime2) as datetime);
    // 2018-12-26 08:10:40.343
    // select cast(cast('2018-12-26 08:10:40.3445000' as datetime2) as datetime);
    // select cast(cast('2018-12-26 08:10:40.3484999' as datetime2) as datetime);
    // 2018-12-26 08:10:40.347
    // select cast(cast('2018-12-26 08:10:40.3485000' as datetime2) as datetime);
    // 2018-12-26 08:10:40.350
    int last7DigitOfNano = nanoSec - (nanoSec / 10000000) * 10000000;
    int roundedNanoSec = 0;
    if (last7DigitOfNano < 1500000) {
        roundedNanoSec = nanoSec - last7DigitOfNano;
    } else if (last7DigitOfNano < 4500000) {
        roundedNanoSec = nanoSec - last7DigitOfNano + 3000000;
    } else if (last7DigitOfNano < 8500000) {
        roundedNanoSec = nanoSec - last7DigitOfNano + 7000000;
    } else {
        roundedNanoSec = nanoSec - last7DigitOfNano + 10000000;
    }
    System.out.println("Before Rounding" + nanoSec);
    System.out.println("After Rounding" + roundedNanoSec);
    return localDateTime.withNano(roundedNanoSec);
}

Ссылка:
1. DateTime2 против DateTime в SQL Server
2. Типы и функции данных даты и времени (Transact-SQL)

0 голосов
/ 09 декабря 2018

Не думаю, что в качестве PK лучше использовать столбец DATETIME.В одну наносекунду могут быть созданы две сущности, которые будут конфликтовать.

Вы должны использовать столбец IDENTITY или SEQUENCE и просто индексировать столбец DATETIME для ускорения запросов.

Обновить

Это устаревшая система - я могуне меняй это.Первичный ключ состоит из 2 полей типа int и этого столбца даты и времени.

В этом случае вам необходимо использовать составной идентификатор.Ознакомьтесь с этой статьей для получения более подробной информации .

Кроме того, я не думаю, что вам нужно это:

registerColumnType(Types.TIMESTAMP, "timestamp");
registerColumnType(Types.DATE, "timestamp");
registerColumnType(Types.TIME, "timestamp");

registerHibernateType(Types.TIMESTAMP, "timestamp");
registerHibernateType(Types.DATE, "timestamp");
registerHibernateType(Types.TIME, "timestamp");

Диалект SQL Server должен обеспечивать правильные сопоставления типов.

Если это не сработает, попробуйте повторить проблему с помощью этого шаблона контрольного примера и откройте проблему Jira.

...