2009-05-26 10 views
23

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.¿Puede Hibernate trabajar con la sintaxis "ACTUALIZACIÓN DE LLAVES DUPLICADAS" de MySQL?

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 ejemplo inventado, digamos que desea contar la cantidad de veces que se ve una historia en un blog. Una forma de hacerlo que 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 eficaz que iniciar una transacción, y el manejo de las excepciones inevitables que se producen cuando las nuevas historias golpean 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 arrojará una excepción porque no comprende las palabras clave específicas de la base de datos. De hecho, a HQL no le gusta ninguna inserción explícita a menos que sea un "INSERT ... SELECT ....".

En segundo lugar, Hibernate limita el SQL a las selecciones solamente. Hibernate lanzará una excepción si intenta llamar al session.createSQLQuery("sql").executeUpdate().

En tercer lugar, el saveOrUpdate de Hibernate no encaja 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 Hibernate?

Respuesta

22

¿Has visto la Anotación de 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 
+1

¿Qué ocurre si desea utilizar un campo de identificación con incremento automático (por ejemplo, en mysql)? Estoy recibiendo errores extraños sobre la base de datos que no devuelve un ID –

2

Esta es una pregunta anterior, 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 la sesión estándar era una sobrecarga innecesaria y que no queríamos que nuestros oyentes de hibernación activaran para 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 utilizar 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 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:

Usted puede utilizar el "inserto en la actualización ... duplicado tecla" mysql comportamiento a través de un sql-pieza de enganche específica para el objeto persistente de hibernación. Se puede definir la cláusula de encargo sql-inserto ya sea a través de anotación (como en la respuesta anterior) o a través de una entidad sql-inserto un mapeo XML de hibernación, 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 cartel original pregunta acerca de 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). Vea this issue para más detalles sobre esa característica en particular.

Así que 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 búsquedas descubrí 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 ajuste final necesario para que este comportamiento funcione es definir un persister personalizado (como se muestra en la asignación xml anterior). En este caso, todo lo que teníamos que hacer era extender SingleTableEntityPersister y 'anular' la inserción de Expectativa. P.ej. Acabo de tachuelas esta clase estática sobre el objeto persistencia y la defino como el persister personalizado 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; 
    } 

} 

Se tomó un buen tiempo de cavar a través de código de hibernación para encontrar esto - yo no era capaz de encontrar cualquier temas en la red con una solución a esto.

+3

¿No se puede lograr lo mismo especificando el atributo "verificar"? Ver http://docs.jboss.org/hibernate/orm/3.6/reference/en-US/html/querysql.html#example-custom-crdu-via-xml Por ejemplo: ' ... 'o' @SQLInsert (sql = "...", check = "none") ' – Shannon

+0

Corrección de arriba, la anotación debe verse así:' @SQLInsert (sql = "...", check = ResultCheckStyle .NONE) ' – Shannon

1

Si está usando Grails, me encontré con esta solución que no requiere mover su dominio de clase en el mundo utilizando JAVA y @SQLInsert anotaciones:

  1. Crea una alfombrilla de configuración de hibernación
  2. Sustituir el PersistentClass Mapa
  3. Agregue su INSERT sql personalizada a las clases persistentes que desee utilizando la tecla ON DUPLICATE.

Por ejemplo, si tiene un objeto Dominio llamada persona y quiere inserciones Para ser INSERT en una llave duplicada ACTUALIZACIÓN debe crear una configuración de este modo:

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 añadir esto como su hibernación configuración en DataSource.groovy:

dataSource { 
    pooled = true 
    driverClassName = "com.mysql.jdbc.Driver" 
    configClass = 'MyCustomConfiguration' 
} 

Sólo una nota que tener cuidado usando LAST_INSERT_ID, ya que esto no se establecerá correctamente si se ejecuta la actualización en lugar de INSERT a menos que establezca explícitamente en el stat elemento, p. 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.

Cuestiones relacionadas