Trouver ou insert basé sur clé unique avec Hibernate
Je suis en train d'écrire une méthode qui renvoie une Hibernate objet basé sur un unique, mais non de la clé primaire. Si l'entité qui existe déjà dans la base de données que je veux renvoyer, mais si ce n'est pas que je veux créer une nouvelle instance et de l'enregistrer avant de le retourner.
Mise à JOUR: Permettez-moi de préciser que l'application que je suis en train d'écrire ce est fondamentalement un traitement par lot des fichiers d'entrée. Le système a besoin de lire un fichier ligne par ligne et insérer des enregistrements dans la db. Le format de fichier est en fait un dénormalisée vue de plusieurs tables dans notre schéma de ce que j'ai à faire est d'analyser l'enregistrement parent soit l'insérer dans la db de sorte que je peux obtenir une nouvelle synthèse de la clé, ou si il existe déjà le sélectionner. Ensuite, je peux ajouter d'autres enregistrements associés dans d'autres tables qui ont des clés étrangères de retour pour cet enregistrement.
La raison pour laquelle ce qui est difficile, c'est que chaque fichier doit être totalement ou non importé importé du tout, c'est à dire toutes les insertions et mises à jour effectuées pour un fichier donné devrait être une partie d'une transaction. C'est assez facile si il n'y a qu'un seul processus qui fait toutes les importations, mais j'aimerais briser ce sur plusieurs serveurs, si possible. En raison de ces contraintes, j'ai besoin d'être en mesure de rester à l'intérieur d'une transaction, mais gérer les exceptions où un dossier existe déjà.
Mappés classe pour les enregistrements parent ressemble à ceci:
@Entity
public class Foo {
@Id
@GeneratedValue(strategy = IDENTITY)
private int id;
@Column(unique = true)
private String name;
...
}
Ma première tentative à la rédaction de cette méthode est comme suit:
public Foo findOrCreate(String name) {
Foo foo = new Foo();
foo.setName(name);
try {
session.save(foo)
} catch(ConstraintViolationException e) {
foo = session.createCriteria(Foo.class).add(eq("name", name)).uniqueResult();
}
return foo;
}
Le problème, c'est quand le nom, je suis à la recherche existe, un org.mise en veille prolongée.AssertionFailure exception est levée par l'appel à uniqueResult(). Le plein de trace de pile est ci-dessous:
org.hibernate.AssertionFailure: null id in com.searchdex.linktracer.domain.LinkingPage entry (don't flush the Session after an exception occurs)
at org.hibernate.event.def.DefaultFlushEntityEventListener.checkId(DefaultFlushEntityEventListener.java:82) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.event.def.DefaultFlushEntityEventListener.getValues(DefaultFlushEntityEventListener.java:190) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.event.def.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:147) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.event.def.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:219) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.event.def.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:99) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.event.def.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:58) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.impl.SessionImpl.autoFlushIfRequired(SessionImpl.java:1185) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.impl.SessionImpl.list(SessionImpl.java:1709) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.impl.CriteriaImpl.list(CriteriaImpl.java:347) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.impl.CriteriaImpl.uniqueResult(CriteriaImpl.java:369) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
Personne ne sait quelle est la cause de cette exception? N'mise en veille prolongée une meilleure façon d'accomplir cette?
Permettez-moi également de manière préventive expliquer pourquoi je suis en insérant d'abord, puis en sélectionnant si et quand cela échoue. Ce besoin de travailler dans un environnement distribué, donc je ne peux pas synchroniser à travers le vérifier pour voir si l'enregistrement existe déjà et de l'insertion. La meilleure façon de le faire est de laisser la base de données gérer cette synchronisation par la vérification de la violation de la contrainte sur chaque insertion.
par distribués, je veux dire que j'ai plusieurs clients sur les différentes machines de l'exécution de ce code contre une seule base de données centralisée. Deux clients différents pourrait tenter d'insérer le même enregistrement dans le même temps, dans ce cas, le "premier" on doit gagner et être conservées et l'autre client doit juste retour de ce qui a déjà été conservées.
n'êtes-vous pas vraiment de voir les conséquences de ne pas briser les données correctement?
Je ne suis pas sûr de ce que vous entendez par "casser les données correctement."
Avez-vous envisager de pré-traitement d'un lot de données de sorte qu'il n'y a pas de conflits? Ou tout simplement de traiter le conflit parties d'abord, dans un seul thread?
OriginalL'auteur Mike Deck | 2011-02-16
Vous devez vous connecter pour publier un commentaire.
J'ai eu un semblable traitement par lots exigence, avec les processus en cours d'exécution sur plusieurs machines virtuelles. L'approche que j'ai utilisée pour cette été comme suit. Il est très bien comme jtahlborn de la suggestion. Cependant, comme vbence souligné, si vous utilisez une transaction IMBRIQUÉE, lorsque vous obtenez la violation de contrainte d'exception, votre session est terminée. Au lieu de cela, j'utilise REQUIRES_NEW, qui suspend la transaction en cours et crée un nouveau, indépendant de la transaction. Si la nouvelle de la transaction est annulée, il ne sera pas affecter la transaction d'origine.
Je suis à l'aide du Printemps TransactionTemplate mais je suis sûr que vous pouvez facilement traduire si vous ne voulez pas une dépendance de Printemps.
Hibernate délégués du sous-jacent gestionnaire de transactions. Spring fournit le soutien pour imbriquées et suspendu les transactions. Un serveur EJB soutiennent également que, par le biais JTA. Il y a aussi quelques autonome JTA fournisseurs comme Atomikos qui sont une option si vous voulez éviter à la fois un EJB serveur d'application ainsi que le Printemps.
Nice job!!! Dans mon cas, si, à l'étape 4 lors de la
found
estnull
,findUnique()
est de retournull
même si l'enregistrement existe réellement dans la base de données (depuis créé par un autre thread). J'ai eu à mettre en œuvre un autreTransactionTemplate
pour lire l'enregistrement. Comprenez-vous pourquoi?Mike Pont: je me demande vraiment comment cela pourrait fonctionner pour vous. Comme le fait remarquer @sp00m un dossier créé dans un autre transaction ne peut pas être visible dans une autre transaction simultanée (dès que leur point de copies instantanées sont créés). Voir la discussion ci-dessous Vlad Mihalceas réponse.
OriginalL'auteur Lawrence McAlpin
Deux solutions me viennent à l'esprit:
C'est ce que les VERROUS de TABLE sont pour
Hibernate ne prend pas en charge les verrous de table, mais c'est la situation quand ils viennent à portée de main. Heureusement, vous pouvez utiliser SQL natif thru
Session.createSQLQuery()
. Par exemple (MySQL):De cette façon, lorsqu'une session (connexion client) obtient le verrou, toutes les autres connexions sont bloquées jusqu'à ce que l'opération se termine et les verrous sont relâchés. Les opérations de lecture sont également bloqué pour les autres connexions, donc inutile de le dire à utiliser seulement en cas d'opérations atomiques.
Ce sujet Hibernate serrures?
Hibernate utilise le verrouillage de niveau ligne. Nous ne pouvons pas l'utiliser directement, car on ne peut pas verrouiller inexistante lignes. Mais nous pouvons créer un mannequin table avec un seul enregistrement, la carte à l'ORM, puis utilisez
SELECT ... FOR UPDATE
style de serrures sur l'objet de synchroniser nos clients. Fondamentalement, nous avons seulement besoin d'être sûr qu'aucune des autres clients (exécutant le même logiciel, avec les mêmes conventions) de le faire tout conflit entre les opérations, tandis que nous travaillons.Votre base de données a savoir la
SELECT ... FOR UPDATE
syntaxe (Hibernate est goig pour l'utiliser), et bien sûr, cela ne fonctionne que si tous vos clients a la même convention (ils ont besoin de verrouiller la même entité factice).Vous pouvez créer tamporary tables en fonction de la
CREATE TABLE
de l'original. Un client peut disposer de la propriété et de faire son travail peasefully. Un autre processus ou d'une procédure stockée peut ensuite copier les enregistrements sur le live DB. Sinon (si vous travaillez avec un grand nombre de tables temporaires databses peut être créé.Quel est le downvote?
Au lieu d'utiliser des verrous explicites qui serait depedent sur SQL natif de la syntaxe et de fonctionnalités, smilar comportement pourrait être atteint réglage de l'JDBC niveau d'isolation de transaction à TRANSACTION_SERIALIZABLE. L'avantage serait que les tables de verrouillage serait alors déterminé par la banque elle-même qui évite explicitement specifiyng les tables pour le verrouiller. Pour ce faire, utilisez la Session.doWork et de connexion.setTransactionIsolation(Connexion.TRANSACTION_SERIALIZABLE)
OriginalL'auteur vbence
La Hibernate documentation sur les transactions et les exceptions stipule que tous les HibernateExceptions sont irrécupérables et que la transaction en cours doit être annulée dès que l'on s'est rencontré. C'est ce qui explique pourquoi le code ci-dessus ne fonctionne pas. En fin de compte vous ne devriez jamais attraper un HibernateException sans quitter la transaction et de la fermeture de la session.
Le seul véritable moyen pour accomplir cela, il semblerait serait de gérer la clôture de la session ancienne et la réouverture d'une nouvelle à l'intérieur de la méthode elle-même. Mise en œuvre d'un findOrCreate méthode qui peut participer à une transaction existante et de est sûre dans un environnement distribué paraît impossible l'utilisation d'Hibernate basé sur ce que j'ai trouvé.
OriginalL'auteur Mike Deck
La solution est en fait très simple. Tout d'abord effectuer une sélection à l'aide de votre nom de valeur. Si un résultat est trouvé, le retour. Si pas, créez-en un nouveau. Dans le cas de la création de l'échec (avec une exception), c'est parce qu'un autre client a ajouté cette même valeur entre vos sélectionner et insérer votre déclaration. C'est donc logique que vous avez une exception. L'attraper, rollback de la transaction et exécutez de nouveau le même code. Parce que la ligne existe déjà, l'instruction select trouver et vous retournerez à votre objet.
Vous pouvez voir ici l'explication des stratégies de verrouillage optimiste et pessimiste avec hibernate ici : http://docs.jboss.org/hibernate/core/3.3/reference/en/html/transactions.html
OriginalL'auteur Nicolas Bousquet
quelques personnes ont mentionné les différentes parties de la stratégie globale. en supposant que vous pouvez généralement s'attendre à trouver un objet existant plus souvent que vous créez un nouvel objet:
juste pour préciser, comme l'a fait remarquer dans une autre réponse, la "niche" de la transaction est en fait une transaction distincte (de nombreuses bases de données ne sont pas même d'un véritable soutien, les transactions imbriquées).
oui, j'ai représenté que. lorsque vous obtenez une violation de contrainte d'avaler et de charger le nouvel objet dans votre extérieur session. c'est le seul moyen de le faire dans un environnement distribué. J'ai fait cette chose de l'utilisation d'hibernate, donc je sais que ça fonctionne.
vous utilisez une transaction imbriquée/session, qui n'a pas d'incidence sur le cadre extérieur.
Il n'était pas évident en fonction de votre organigramme.
OriginalL'auteur jtahlborn
C'est une grande question, j'ai donc décidé d'écrire un article pour expliquer tout ça en détail.
Comme je l'ai expliqué dans ce gratuit chapitre de mon livre de Haute Performance, de Persistance Java, vous devez utiliser UPSERT ou de FUSIONNER pour atteindre cet objectif.
Cependant, Hibernate n'offre pas de support pour cette construction, si vous avez besoin d'utiliser jOOQ à la place.
L'appel de cette méthode sur PostgreSQL:
Donne les instructions SQL suivantes:
Sur Oracle et SQL Server, jOOQ utilisera
MERGE
alors que sur MySQL, il va utiliser leON DUPLICATE KEY
.La simultanéité mécanisme est assuré par le niveau de la ligne de mécanisme de verrouillage emplyed lors de l'insertion, mise à jour ou deletinga enregistrement,w hich vous pouvez afficher dans le diagramme suivant:
Code disponible sur GitHub.
returning()
à laINSERT
déclaration, et il va vous obtenir ce record en une seule instruction.Cool! Je ne savais pas à propos de la
RETURNING
clause dans PostgreSQL.Que faire si il y a deux transactions simultanées qui tentent de créer le même enregistrement et par la suite (toujours à l'intérieur de la transaction), faire quelques trucs avec le nouveau record? Pas de problème à l'intérieur de la réussir de la transaction, mais dans la transaction qui ignore le double de la clé, le dossier créé à l'autre de la transaction n'est pas visible/sélectionnable, je pense. Suis-je tort? Comment cela doit-il être géré?
Pas de problème. La première Tx prendre un verrou de niveau ligne sur l'entrée avec cette id spécifié, et le second Tx bloquera jusqu'à ce que la première Tx libérer le verrou soit par le biais d'un commit ou un rollback. Par conséquent, la deuxième Tx reprendra qu'après Tx1 s'engage donc il n'y a aucun moyen, il y aura un conflit. Pour plus de détails, consultez cet article.
Pas de conflits, je suis d'accord, mais à l'intérieur la deuxième Tx de l'entrée qui a été créé dans la première Tx n'est pas visible/sélectionnable et ne peut donc pas être utilisé à l'intérieur la deuxième Tx.
OriginalL'auteur Vlad Mihalcea
Eh bien, voici une façon de le faire - mais il n'est pas approprié pour toutes les situations.
name
. Ajouter un horodatage est mise à jour à chaque insertion.findOrCreate()
, n'est pas la peine de vérifier si l'entité avec le même nom existe déjà - il suffit d'insérer un nouveau à chaque fois.name
, il peut y avoir 0 ou plus avec un nom donné, de sorte que vous suffit de sélectionner le plus récent.La bonne chose à propos de cette méthode est qu'elle ne nécessite pas de blocage, donc tout devrait fonctionner assez rapide. L'inconvénient est que votre base de données sera jonché avec les enregistrements obsolètes, de sorte que vous peut avoir quelque chose à faire quelque part d'autre à traiter avec eux. Aussi, si d'autres tables se référer à Toto par son
id
, puis ce sera à vis de ces relations.OriginalL'auteur Mike Baranczak
Peut-être que vous devriez changer votre stratégie:
D'abord trouver l'utilisateur avec le nom et seulement si l'utilisateur thoes existe pas, créez-la.
Peut-être que vous n'avez pas à synchroniser. Si vous faites une recherche tout d'abord, vous allez créer seulement si l'utilisateur n'existe pas et si, dans ce moment, quelqu'un créer un avec ce nom, la base de données gérer cela pour vous et quand vous enregistrez, vous obtiendrez une exception, alors que vous venez de faire quelque chose à ce sujet.
quand vous dites "vous obtiendrez une exception, alors que vous venez de faire quelque chose", ce que vous proposez-je faire? À ce point, j'aurais alors besoin d'exécuter un autre select pour récupérer l'entrée de la db, sauf qu'après l'exception est levée, la session est mort et je suis nécessaires à la restauration de la transaction de retour. Dans ce scénario, je suis de retour à la case départ résoudre exactement le même problème j'ai posé la question en premier lieu.
OriginalL'auteur Iogui
Je voudrais essayer de la stratégie suivante:
A. Démarrer une opération principale (au temps 1)
B. Démarrer une sous-transaction (au temps 2)
Maintenant, tout objet créé après le temps 1 ne sera pas visible dans l'opération principale. Ainsi, lorsque vous vous
C. Créer de nouveaux course-état de l'objet, de s'engager sous-transaction
D. Gérer les conflits par le démarrage d'une nouvelle sous-transaction (temps 3) et l'obtention de l'objet à partir d'une requête (le sous-transaction du point B est maintenant hors de portée).
seulement retourner l'objet clé primaire, puis utiliser l'EntityManager.getReference(..) pour obtenir l'objet que vous allez utiliser pour l'opération principale. Vous pouvez également démarrer l'opération principale après le D; il n'est pas totalement clair pour moi dans combien de conditions de course, vous aurez au sein de votre opération principale, mais le ci-dessus devrait permettre de n fois B-C-D dans un "grand" de la transaction.
Note de ce que vous pourriez voulez faire du multi-threading (un thread par CPU) et puis vous pouvez probablement réduire ce problème considérablement à l'aide d'un partagées statique de cache, pour ces types de conflits - et le point 2 peut être gardé 'optimiste', c'est à dire ne pas faire un .trouver des (..).
Modifier: Pour une nouvelle opération, vous avez besoin d'une interface d'EJB appel de la méthode annotée avec le type d'opération REQUIRES_NEW.
Modifier: vérifiez que les getReference(..) fonctionne comme je le pense.
OriginalL'auteur ThomasRS