sql >> Base de Datos >  >> NoSQL >> MongoDB

Cascada personalizada en Spring Data MongoDB

1. Resumen

Este tutorial continuará explorando algunas de las funciones principales de Spring Data MongoDB:@DBRef eventos de anotación y ciclo de vida.

2. @DBRef

El marco de mapeo no admite almacenamiento de relaciones padre-hijo y documentos incrustados dentro de otros documentos. Sin embargo, lo que podemos hacer es almacenarlos por separado y usar un DBRef para hacer referencia a los documentos.

Cuando el objeto se cargue desde MongoDB, esas referencias se resolverán rápidamente y obtendremos un objeto mapeado que se verá igual que si hubiera estado almacenado incrustado en nuestro documento maestro.

Veamos un poco de código:

@DBRef
private EmailAddress emailAddress;

Dirección de correo electrónico parece:

@Document
public class EmailAddress {
    @Id
    private String id;
    
    private String value;
    
    // standard getters and setters
}

Tenga en cuenta que el marco de mapeo no maneja operaciones en cascada . Entonces, por ejemplo, si activamos un guardar en un padre, el hijo no se guardará automáticamente; tendremos que activar explícitamente el guardado en el hijo si queremos guardarlo también.

Aquí es precisamente donde los eventos del ciclo de vida son útiles .

3. Eventos del ciclo de vida

Spring Data MongoDB publica algunos eventos de ciclo de vida muy útiles, como onBeforeConvert, onBeforeSave, onAfterSave, onAfterLoad y onAfterConvert.

Para interceptar uno de los eventos, necesitamos registrar una subclase de AbstractMappingEventListener y anular uno de los métodos aquí. Cuando se envíe el evento, se llamará a nuestro oyente y se pasará el objeto de dominio.

3.1. Ahorro en cascada básico

Veamos el ejemplo que teníamos antes:guardar el usuario con la dirección de correo electrónico . Ahora podemos escuchar onBeforeConvert evento que se llamará antes de que un objeto de dominio entre en el convertidor:

public class UserCascadeSaveMongoEventListener extends AbstractMongoEventListener<Object> {
    @Autowired
    private MongoOperations mongoOperations;

    @Override
    public void onBeforeConvert(BeforeConvertEvent<Object> event) { 
        Object source = event.getSource(); 
        if ((source instanceof User) && (((User) source).getEmailAddress() != null)) { 
            mongoOperations.save(((User) source).getEmailAddress());
        }
    }
}

Ahora solo necesitamos registrar el oyente en MongoConfig :

@Bean
public UserCascadeSaveMongoEventListener userCascadingMongoEventListener() {
    return new UserCascadeSaveMongoEventListener();
}

O como XML:

<bean class="org.baeldung.event.UserCascadeSaveMongoEventListener" />

Y tenemos la semántica en cascada lista, aunque solo para el usuario.

3.2. Una implementación genérica en cascada

Ahora mejoremos la solución anterior al hacer que la funcionalidad en cascada sea genérica. Comencemos definiendo una anotación personalizada:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface CascadeSave {
    //
}

Ahora, trabajemos en nuestro oyente personalizado para manejar estos campos de forma genérica y no tener que convertir a ninguna entidad en particular:

public class CascadeSaveMongoEventListener extends AbstractMongoEventListener<Object> {

    @Autowired
    private MongoOperations mongoOperations;

    @Override
    public void onBeforeConvert(BeforeConvertEvent<Object> event) { 
        Object source = event.getSource(); 
        ReflectionUtils.doWithFields(source.getClass(), 
          new CascadeCallback(source, mongoOperations));
    }
}

Así que estamos usando la utilidad de reflexión de Spring y estamos ejecutando nuestra devolución de llamada en todos los campos que cumplen con nuestros criterios:

@Override
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
    ReflectionUtils.makeAccessible(field);

    if (field.isAnnotationPresent(DBRef.class) && 
      field.isAnnotationPresent(CascadeSave.class)) {
    
        Object fieldValue = field.get(getSource());
        if (fieldValue != null) {
            FieldCallback callback = new FieldCallback();
            ReflectionUtils.doWithFields(fieldValue.getClass(), callback);

            getMongoOperations().save(fieldValue);
        }
    }
}

Como puede ver, estamos buscando campos que tengan tanto el DBRef anotación así como CascadeSave . Una vez que encontramos estos campos, guardamos la entidad secundaria.

Veamos el FieldCallback clase que estamos usando para verificar si el niño tiene un @Id anotación:

public class FieldCallback implements ReflectionUtils.FieldCallback {
    private boolean idFound;

    public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
        ReflectionUtils.makeAccessible(field);

        if (field.isAnnotationPresent(Id.class)) {
            idFound = true;
        }
    }

    public boolean isIdFound() {
        return idFound;
    }
}

Finalmente, para que todo funcione en conjunto, por supuesto, necesitamos emailAddress campo para que ahora se anote correctamente:

@DBRef
@CascadeSave
private EmailAddress emailAddress;

3.3. La prueba de la cascada

Ahora echemos un vistazo a un escenario:guardamos un Usuario con dirección de correo electrónico y la operación de guardado se conecta en cascada a esta entidad incrustada automáticamente:

User user = new User();
user.setName("Brendan");
EmailAddress emailAddress = new EmailAddress();
emailAddress.setValue("[email protected]");
user.setEmailAddress(emailAddress);
mongoTemplate.insert(user);

Revisemos nuestra base de datos:

{
    "_id" : ObjectId("55cee9cc0badb9271768c8b9"),
    "name" : "Brendan",
    "age" : null,
    "email" : {
        "value" : "[email protected]"
    }
}