Как молча обрезать строки при их хранении, когда они длиннее определения длины столбца? - PullRequest
33 голосов
/ 04 октября 2011

У меня есть веб-приложение, использующее EclipseLink и MySQL для хранения данных. Некоторые из этих данных являются строками, т.е. varchars в БД. В коде сущностей строки имеют такие атрибуты:

@Column(name = "MODEL", nullable = true, length = 256)
private String model;

База данных не создается eclipseLink из кода, но длина соответствует длине varchar в БД. Когда длина таких строковых данных больше, чем атрибут длины, возникает исключение во время вызова javax.persistence.EntityTransaction.commit ():

javax.persistence.RollbackException: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.1.0.v20100614-r7608): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'MODEL' at row 1

Затем транзакция откатывается. Хотя я понимаю, что это поведение по умолчанию, это не то, что я хочу. Я хотел бы, чтобы данные были беззвучно обрезаны, а транзакция зафиксирована.

Могу ли я сделать это, не добавляя вызов подстроки в каждый метод набора строковых данных для соответствующих объектов?

Ответы [ 12 ]

17 голосов
/ 22 февраля 2013

Можно обрезать строку в соответствии с аннотациями JPA в установщике для соответствующего поля:

public void setX(String x) {
    try {
        int size = getClass().getDeclaredField("x").getAnnotation(Column.class).length();
        int inLength = x.length();
        if (inLength>size)
        {
            x = x.substring(0, size);
        }
    } catch (NoSuchFieldException ex) {
    } catch (SecurityException ex) {
    }
    this.x = x;
}

Сама аннотация должна выглядеть так:

@Column(name = "x", length=100)
private String x;

(на основе https://stackoverflow.com/a/1946901/16673)

Аннотации могут быть воссозданы из базы данных, если база данных изменится, как указано в комментарии к https://stackoverflow.com/a/7648243/16673

10 голосов
/ 22 февраля 2013

У вас есть разные решения и ложные решения.

Использование триггера или любого трюка на уровне базы данных

Это создаст несоответствие между объектами в ORM и их сериализованной формой в БД. Если вы используете кеширование второго уровня: это может привести к множеству неприятностей. На мой взгляд, это не реальное решение для общего случая использования.

С использованием предварительной вставки, крюков перед обновлением

Вы будете молча изменять пользовательские данные непосредственно перед их сохранением. Таким образом, ваш код может вести себя по-разному в зависимости от того, что объект уже сохранен или нет. Это может вызвать проблемы тоже. Кроме того, вы должны быть осторожны с порядком вызова ловушек: убедитесь, что ваш "field-truncator-hook" - самый первый, вызываемый провайдером персистентности.

Использование aop для перехвата вызова сеттеров

Это решение будет более или менее тихо изменять пользовательский / автоматический ввод, но ваши объекты не будут изменены после того, как вы используете их для выполнения бизнес-логики. Так что это более приемлемо, чем два предыдущих решения, но сеттеры не будут следовать контракту обычных сеттеров. Кроме того, если вы используете инъекцию поля: это обойдет аспект (в зависимости от вашей конфигурации провайдер jpa может использовать инъекцию поля. Большую часть времени: Spring использует сеттеры. Я думаю, что некоторые другие платформы могут использовать инъекцию поля, так что даже если вы не используете не используйте его явно, чтобы быть в курсе базовой реализации используемых вами фреймворков).

Использование aop для перехвата модификации поля

Аналогично предыдущему решению, за исключением того, что ввод поля также будет перехвачен аспектом. (обратите внимание, что я никогда не писал аспект, делающий подобные вещи, но я думаю, что это возможно)

Добавление уровня контроллера для проверки длины поля перед вызовом установщика

Вероятно, лучшее решение с точки зрения целостности данных. Но это может потребовать много рефакторинга. Для общего случая использования это (на мой взгляд) единственно приемлемое решение.

В зависимости от вашего варианта использования вы можете выбрать любое из этих решений. Помните о недостатках.

7 голосов
/ 31 января 2012

Есть еще один способ сделать это, может быть, даже быстрее (по крайней мере, он работает на 5-й версии MySql):

Сначала проверьте настройку sql_mode: есть подробное описание, как это сделать . Этот параметр должен иметь значение "" для окон и "режимы" для Unix.

Это мне не помогло, поэтому я нашел другую настройку, на этот раз в jdbc:

jdbcCompliantTruncation=false. 

В моем случае (я использовал постоянство) это определяется в файле persistence.xml:

<property name="javax.persistence.jdbc.url" value="jdbc:mysql://127.0.0.1:3306/dbname?jdbcCompliantTruncation=false"/>

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

Обратите внимание : помните, что установив sql_mode, как описано выше, вы отключаете важные проверки базы данных, поэтому, пожалуйста, сделайте это осторожно.

5 голосов
/ 22 февраля 2013

Если вы хотите сделать это на основе поля за полем, а не на глобальном уровне, то вы можете создать пользовательское сопоставление типов, которое усекает значение до заданной длины, прежде чем вставить его в таблицу. Затем вы можете прикрепить конвертер к сущности с помощью аннотации, такой как:

@Converter(name="myConverter", class="com.example.MyConverter")

и в соответствующие поля через:

@Convert("myConverter")

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

5 голосов
/ 04 октября 2011

Для этого вы можете использовать событие дескриптора PreInsert / PreUpdate или, возможно, просто использовать события JPA PreInsert и PreUpdate.

Просто проверьте размер полей и обрежьте их в коде события.

При необходимости вы можете получить размер поля из базы данных дескриптора DatabaseField или использовать отражение Java для получения его из аннотации.

Вероятно, было бы даже лучше просто выполнить усечение в ваших методах set. Тогда вам не нужно беспокоиться о событиях.

Вы также можете обрезать его в базе данных, проверить настройки MySQL или использовать триггер.

2 голосов
/ 22 февраля 2013

Две очень важные особенности дизайна пользовательского интерфейса:

  1. Вы никогда не должны молча изменять данные пользователя - пользователь должен знать, контролировать и соглашаться с такими изменениями.
  2. Вы никогда не должны позволять вводить данные, которые не могут быть обработаны - используйте атрибуты UI / HTML и проверку, чтобы ограничить данные допустимыми значениями

Ответ на вашу проблему очень прост. Просто ограничьте поля ввода вашего интерфейса до 256 символов, чтобы соответствовать длине поля базы данных:

<input type="text" name="widgetDescription" maxlength="256">

Это системное ограничение - пользователь не должен иметь возможность вводить больше данных, чем это. Если этого недостаточно, измените базу данных.

1 голос
/ 22 февраля 2013

Возможно АОП поможет:

Перехватите весь метод set в вашем JavaBean / POJO, а затем получите поле, которое нужно установить. Убедитесь, что поле помечено @Column, а тип поля - String. Затем обрежьте поле, если оно слишком длинное, чем length.

1 голос
/ 22 февраля 2013

Поскольку исключение возникает на уровне базы данных во время процесса commit (), триггер перед вставкой в ​​целевую таблицу / таблицы может обрезать новые значения до их фиксации, минуя ошибку.

1 голос
/ 20 февраля 2013

Вы можете настроить свою базу данных для работы в нестрогом режиме, как описано здесь: Автоматически обрезать длину строки, представленной в MySQL

Обратите внимание, что это может также отменить другие проверки, поэтому будьте осторожны с тем, что вы хотите

1 голос
/ 04 октября 2011

Я ничего не знаю о EclipseLink, но в Hibernate это выполнимо - вы можете сделать org.hibernate.Interceptor и в методе onFlushDirty изменить объект currentState, используя метаданные объекта.

...