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?
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
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.
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.
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.
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.
Deux caractéristiques très importantes de la conception de l'interface utilisateur:
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.
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
.
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é.
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
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.
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
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;
}
}