java - for - ¿Puede Hibernate trabajar con la sintaxis "ACTUALIZACIÓN DE LLAVES DUPLICADAS" de MySQL?
orm para java (3)
MySQL admite una sintaxis " INSERT ... ON DUPLICATE KEY UPDATE ...
" que le permite insertar "ciegamente" en la base de datos y volver a actualizar el registro existente, si existe.
Esto es útil cuando desea un aislamiento rápido de las transacciones y los valores que desea actualizar dependen de los valores que ya están en la base de datos.
Como un ejemplo artificial, digamos que quieres contar la cantidad de veces que se ve una historia en un blog. Una forma de hacer eso con esta sintaxis podría ser:
INSERT INTO story_count (id, view_count) VALUES (12345, 1)
ON DUPLICATE KEY UPDATE set view_count = view_count + 1
Esto será más eficiente y más efectivo que comenzar una transacción y manejar las inevitables excepciones que ocurren cuando las nuevas historias llegan a la primera página.
¿Cómo podemos hacer lo mismo o lograr el mismo objetivo con Hibernate?
En primer lugar, el analizador HQL de Hibernate lanzará una excepción porque no comprende las palabras clave específicas de la base de datos. De hecho, a HQL no le gustan las inserciones explícitas a menos que sea un " INSERT ... SELECT ....
".
En segundo lugar, Hibernate limita el SQL a solo las selecciones. Hibernate lanzará una excepción si intentas llamar a session.createSQLQuery("sql").executeUpdate()
.
En tercer lugar, saveOrUpdate
de Hibernate no se ajusta a la factura en este caso. Sus pruebas pasarán, pero luego obtendrá fallas de producción si tiene más de un visitante por segundo.
¿Realmente tengo que subvertir a Hibernate?
¿Has mirado la anotación Hibernate @SQLInsert ?
@Entity
@Table(name="story_count")
@SQLInsert(sql="INSERT INTO story_count(id, view_count) VALUES (?, ?)
ON DUPLICATE KEY UPDATE view_count = view_count + 1" )
public class StoryCount
Esta es una vieja pregunta, pero estaba teniendo un problema similar y pensé que agregaría a este tema. Necesitaba agregar un registro a un escritor de registro de auditoría de StatelessSession existente. La implementación existente usaba una sesión sin estado porque el comportamiento de almacenamiento en caché de la implementación de sesión estándar era una sobrecarga innecesaria y no queríamos que nuestros oyentes de hibernación activaran la escritura del registro de auditoría. Esta implementación se trataba de lograr el mayor rendimiento de escritura posible sin interacciones.
Sin embargo, el nuevo tipo de registro necesitaba usar un tipo de comportamiento insert-else-update, donde tenemos la intención de actualizar las entradas de registro existentes con un tiempo de transacción como un tipo de comportamiento de "marcado". En una sesión sin estado, saveOrUpdate () no se ofrece, por lo que necesitamos implementar manualmente la inserción-else-update.
A la luz de estos requisitos:
Puede usar el comportamiento mysql "insertar ... en actualización de clave duplicada" a través de una inserción sql personalizada para el objeto persistente de hibernación. Puede definir la cláusula sql-insert personalizada mediante anotación (como en la respuesta anterior) o mediante una entidad sql-insert un mapeo hibernate xml, por ejemplo:
<class name="SearchAuditLog" table="search_audit_log" persister="com.marin.msdb.vo.SearchAuditLog$UpsertEntityPersister">
<composite-id name="LogKey" class="SearchAuditLog$LogKey">
<key-property
name="clientId"
column="client_id"
type="long"
/>
<key-property
name="objectType"
column="object_type"
type="int"
/>
<key-property
name="objectId"
column="object_id"
/>
</composite-id>
<property
name="transactionTime"
column="transaction_time"
type="timestamp"
not-null="true"
/>
<!-- the ordering of the properties is intentional and explicit in the upsert sql below -->
<sql-insert><![CDATA[
insert into search_audit_log (transaction_time, client_id, object_type, object_id)
values (?,?,?,?) ON DUPLICATE KEY UPDATE transaction_time=now()
]]>
</sql-insert>
El póster original pregunta sobre MySQL específicamente. Cuando implementé el comportamiento insert-else-update con mysql obtuve excepciones cuando se ejecutó la ''ruta de actualización'' de sql. Específicamente, mysql informaba que se cambiaron 2 filas cuando solo se actualizó 1 fila (ostensiblemente porque la fila existente es eliminada y la nueva fila está insertada). Consulte este problema para obtener más detalles sobre esa característica en particular.
Por lo tanto, cuando la actualización devolvió 2 veces el número de filas afectadas para hibernar, Hibernar lanzaba una excepción BatchedTooManyRowsAffectedException, retrotraía la transacción y propocionaba la excepción. Incluso si tuviera que atrapar la excepción y manejarla, la transacción ya se había retrotraído en ese punto.
Después de algunas excavaciones encontré que esto era un problema con la entidad persister que hibernate estaba usando. En mi caso, hibernate usa SingleTableEntityPersister, que define una Expectativa de que el número de filas actualizadas debe coincidir con el número de filas definidas en la operación por lotes.
El último ajuste necesario para que este comportamiento funcione es definir un persist personalizado (como se muestra en el mapeo xml anterior). En este caso, todo lo que teníamos que hacer era extender SingleTableEntityPersister y ''anular'' la inserción de Expectativa. Por ejemplo, simplemente viñeteé esta clase estática en el objeto de persistencia y la defino como la persistencia personalizada en el mapeo de hibernación:
public static class UpsertEntityPersister extends SingleTableEntityPersister {
public UpsertEntityPersister(PersistentClass arg0, EntityRegionAccessStrategy arg1, SessionFactoryImplementor arg2, Mapping arg3) throws HibernateException {
super(arg0, arg1, arg2, arg3);
this.insertResultCheckStyles[0] = ExecuteUpdateResultCheckStyle.NONE;
}
}
Me tomó bastante tiempo buscar en el código de hibernación para encontrar esto: no pude encontrar ningún tema en la red con una solución para esto.
Si está utilizando Grails, encontré esta solución que no requería mover su clase de dominio al mundo JAVA y usar @SQLInsert anotaciones:
- Crear una configuración de hibernación personalizada
- Anular el mapa PersistentClass
- Agregue su sql INSERT personalizado a las clases persistentes que desee utilizando la tecla ON DUPLICATE.
Por ejemplo, si tiene un objeto de Dominio llamado Persona y desea INSERTAR para INSERTAR EN ACTUALIZACIÓN DE LLAVE DUPLICADA, creará una configuración como esta:
public class MyCustomConfiguration extends GrailsAnnotationConfiguration {
public MyCustomConfiguration() {
super();
classes = new HashMap<String, PersistentClass>() {
@Override
public PersistentClass put(String key, PersistentClass value) {
if (Person.class.getName().equalsIgnoreCase(key)) {
value.setCustomSQLInsert("insert into person (version, created_by_id, date_created, last_updated, name) values (?, ?, ?, ?, ?) on duplicate key update id=LAST_INSERT_ID(id)", true, ExecuteUpdateResultCheckStyle.COUNT);
}
return super.put(key, value);
}
};
}
y agregue esto como su configuración de Hibernate en DataSource.groovy:
dataSource {
pooled = true
driverClassName = "com.mysql.jdbc.Driver"
configClass = ''MyCustomConfiguration''
}
Solo una nota para tener cuidado al usar LAST_INSERT_ID, ya que NO se establecerá correctamente si se ejecuta UPDATE en lugar de INSERT a menos que lo establezcas explícitamente en la declaración, por ej., Id = LAST_INSERT_ID (id). No he verificado de dónde GORM obtiene la identificación, pero supongo que está usando LAST_INSERT_ID.
Espero que esto ayude.