web-dev-qa-db-fra.com

Comment tronquer silencieusement des chaînes en les stockant lorsqu'elles sont plus longues que la définition de longueur de colonne?

J'ai une application Web qui utilise EclipseLink et MySQL pour stocker des données . Certaines de ces données sont des chaînes, c.-à-d. Varchars dans la base de données . Dans le code des entités, les chaînes ont des attributs tels que:

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

EclipseLink n'a pas créé la base de données à partir du code, mais sa longueur correspond à la longueur de varchar dans le DB . Lorsque la longueur d'une telle donnée de chaîne est supérieure à l'attribut length, une exception est déclenchée lors de l'appel de 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

Ensuite, la transaction est annulée… Bien que je comprenne que c'est le comportement par défaut, ce n'est pas celui que je veux… .. Je voudrais que les données soient tronquées de manière silencieuse et que la transaction soit validée.

Puis-je faire cela sans ajouter un appel à la sous-chaîne à chaque méthode définie de données de chaîne pour les entités concernées?

31
Fabien

On peut tronquer une chaîne en fonction des annotations JPA dans le setter du champ correspondant:

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;
}

L'annotation elle-même devrait ressembler à:

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

(Basé sur https://stackoverflow.com/a/1946901/16673 )

Les annotations peuvent être recréées à partir de la base de données si celle-ci change, comme indiqué dans le commentaire à https://stackoverflow.com/a/7648243/16673

17
Suma

Vous avez différentes solutions et de fausses solutions.

En utilisant un déclencheur ou une astuce de niveau base de données

Cela créera une incohérence entre les objets de l'ORM et leur forme sérialisée dans la base de données. Si vous utilisez la mise en cache de deuxième niveau, cela peut entraîner de nombreux problèmes. À mon avis, ce n'est pas une vraie solution pour un cas d'utilisation général.

Utilisation de crochets de pré-insertion et de pré-mise à jour

Vous allez modifier en silence les données utilisateur juste avant de les conserver. Votre code peut donc se comporter différemment selon que l'objet est déjà persistant ou non. Cela peut aussi causer des problèmes. De plus, vous devez faire attention à l'ordre d'appel des hooks: assurez-vous que votre "hook-truncator-hook" est le tout premier appelé par le fournisseur de persistance.

Utilisation de aop pour intercepter les appels destinés aux setters

Cette solution modifiera de manière plus ou moins silencieuse la saisie utilisateur/automatique, mais vos objets ne seront pas modifiés après leur utilisation dans une logique métier. C'est donc plus acceptable que les 2 solutions précédentes, mais les setters ne suivront pas le contrat des setters habituels. De plus, si vous utilisez l’injection sur le terrain: cela contournera l’aspect (en fonction de votre configuration, le fournisseur jpa peut utiliser l’injection sur le terrain. La plupart du temps: Spring utilise des régulateurs. Je suppose que d’autres frameworks peuvent utiliser l’injection sur le terrain. ne l'utilisez pas explicitement, soyez conscient de l'implémentation sous-jacente des frameworks que vous utilisez).

Utilisation de aop pour intercepter la modification de champ

Semblable à la solution précédente, sauf que l'injection sur le terrain sera également interceptée par l'aspect. (notez que je n'ai jamais écrit un aspect en faisant ce genre de choses, mais je pense que c'est faisable)

Ajout d'une couche de contrôleur pour vérifier la longueur du champ avant d'appeler le séparateur

Probablement la meilleure solution en termes d'intégrité des données. Mais cela peut nécessiter beaucoup de refactoring. Pour un cas d'usage général, c'est (à mon avis) la seule solution acceptable.

Selon votre cas d'utilisation, vous pouvez choisir l'une de ces solutions. Soyez conscient des inconvénients.

10
ben75

Il existe un autre moyen de le faire, peut-être même plus rapide (du moins cela fonctionne sur la 5ème version de MySql):

Commencez par vérifier votre paramètre sql_mode: une description détaillée explique comment procéder . Ce paramètre doit avoir la valeur "" pour Windows et "modes" pour Unix.

Cela ne m'a pas aidé, alors j'ai trouvé un autre paramètre, cette fois dans jdbc: 

jdbcCompliantTruncation=false. 

Dans mon cas (j'ai utilisé la persistance), il est défini dans le fichier persistence.xml:

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

Ces 2 paramètres ne fonctionnent que ensemble, j'ai essayé de les utiliser séparément et sans effet.

Remarque: rappelez-vous qu'en réglant sql_mode comme décrit ci-dessus, vous désactivez les contrôles importants de la base de données. Veuillez donc le faire avec prudence.

6
Choufler

Si vous souhaitez le faire champ par champ plutôt que globalement, vous pourrez alors créer un mappage de type personnalisé qui tronque la valeur à la longueur donnée avant de l'insérer dans la table. Ensuite, vous pouvez attacher le convertisseur à l'entité par une annotation telle que:

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

et aux champs correspondants via:

@Convert("myConverter")

Ceci est vraiment destiné à prendre en charge les types SQL personnalisés, mais cela pourrait également fonctionner pour les champs de type varchar normaux. Ici est un tutoriel sur la fabrication de l'un de ces convertisseurs.

5
Eric Galluzzo

Vous pouvez utiliser un événement Descriptor PreInsert/PreUpdate ou éventuellement simplement utiliser des événements JPA PreInsert et PreUpdate.

Il suffit de vérifier la taille des champs et de les tronquer dans le code de l'événement.

Si nécessaire, vous pouvez obtenir la taille du champ à partir de DatabaseField du mappage du descripteur ou utiliser la réflexion Java pour l'obtenir à partir de l'annotation.

Il serait probablement préférable de ne faire que la troncature dans vos méthodes set. Ensuite, vous n'avez pas besoin de vous inquiéter des événements.

Vous pourrez également le tronquer sur la base de données, vérifier vos paramètres MySQL ou éventuellement utiliser un déclencheur.

5
James

Deux caractéristiques très importantes de la conception de l'interface utilisateur:

  1. Vous ne devez jamais modifier en silence les données de l'utilisateur. L'utilisateur doit être conscient, en contrôle et consentir à de telles modifications.
  2. Vous ne devez jamais autoriser la saisie de données impossibles à gérer. Utilisez des attributs et une validation UI/HTML pour limiter les données à des valeurs légales.

La réponse à votre problème est très simple. Limitez simplement vos champs de saisie d'interface utilisateur à 256 caractères, afin qu'ils correspondent à la longueur du champ de base de données:

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

Ceci est une limitation du système - l'utilisateur ne devrait pas pouvoir entrer plus de données que cela. Si cela est insuffisant, changez la base de données.

2
Glen Best

Peut-être que AOP aidera:

Intercepter toutes les méthodes de jeu dans votre JavaBean/POJO, puis obtenir le fichier à définir. Vérifiez si le champ est annoté avec @Column et si le type de champ est String. Puis tronquez le champ s'il est trop long que length.

1
lichengwu

Je ne connais rien à EclipseLink, mais dans Hibernate, c'est faisable: vous pouvez créer un org.hibernate.Interceptor et une méthode onFlushDirty pour modifier l'objet currentState de l'objet à l'aide de métadonnées d'entité.

1
kan

Vous pouvez configurer votre base de données pour qu'elle fonctionne en mode non strict, comme expliqué ci-après: Rognage automatique de la longueur de la chaîne soumise à MySQL

Notez que cela peut aussi annuler d'autres validations, alors faites attention à ce que vous souhaitez

1
Doron Manor

Comme l'exception semble être levée au niveau de la base de données au cours du processus commit (), un déclencheur avant insertion sur la/les table (s) cible (s) pourrait tronquer les nouvelles valeurs avant leur validation, en contournant l'erreur.

1
Darrell Teague

Il y avait déjà réponse mentionnée Convertisseurs , mais je veux ajouter plus de détails. Ma réponse suppose également que les convertisseurs de JPA ne sont pas spécifiques à EclipseLink.

Dans un premier temps, créez cette classe - convertisseur de type spécial dont la responsabilité sera de tronquer la valeur au moment de la persistance:

import javax.persistence.AttributeConverter;
import javax.persistence.Convert;

@Convert
public class TruncatedStringConverter implements AttributeConverter<String, String> {
  private static final int LIMIT = 999;

  @Override
  public String convertToDatabaseColumn(String attribute) {
    if (attribute == null) {
      return null;
    } else if (attribute.length() > LIMIT) {
      return attribute.substring(0, LIMIT);
    } else {
      return attribute;
    }
  }

  @Override
  public String convertToEntityAttribute(String dbData) {
    return dbData;
  }
}

Ensuite, vous pouvez l'utiliser dans vos entités comme ceci:

@Entity(name = "PersonTable")
public class MyEntity {
    
    @Convert(converter = TruncatedStringConverter.class)
    private String veryLongValueThatNeedToBeTruncated;
 
    //...
}

Article connexe sur les convertisseurs JPA: http://www.baeldung.com/jpa-attribute-converters

0
Aleksei Egorov

Une autre option consiste à déclarer une constante et à la référencer partout où vous avez besoin de cette longueur en commençant par l'annotation @Column elle-même.

Cette constante peut ensuite être transmise à une fonction tronquée ou à une fonction de validation qui génère une exception préventive si la chaîne transmise est trop longue . Cette constante peut également être réutilisée sur d'autres couches telles qu'une interface utilisateur.

Par exemple:

@Entity
public class MyEntity {
    public static final int NAME_LENGTH=32;

    private Long id;
    private String name;

    @Id @GeneratedValue
    public Long getId() {
        return id;
    }
    protected void setId(Long id) {
        this.id = id;
    }

    @Column(length=NAME_LENGTH)
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = JpaUtil.truncate(name, NAME_LENGTH);
    }
}

public class JpaUtil {
    public static String truncate(String value, int length) {
        return value != null && value.length() > length ? value.substring(0, length) : value;
    }
}
0
P. Cédric