Cascata personalizada no Spring Data MongoDB

Cascata personalizada no Spring Data MongoDB

1. Visão geral

Este tutorial continuará a explorar alguns dos principais recursos do Spring Data MongoDB - a anotação@DBRef e eventos de ciclo de vida.

2. @DBRef

A estrutura de mapeamento não oferece suporte astoring parent-child relationse documentos incorporados em outros documentos. O que podemos fazer é - podemos armazená-los separadamente e usarDBRef para se referir aos documentos.

Quando o objeto é carregado do MongoDB, essas referências serão prontamente resolvidas e obteremos de volta um objeto mapeado que parece o mesmo que se tivesse sido armazenado incorporado em nosso documento mestre.

Vejamos alguns códigos:

@DBRef
private EmailAddress emailAddress;

EmailAddress se parece com:

@Document
public class EmailAddress {
    @Id
    private String id;

    private String value;

    // standard getters and setters
}

Observe que a estrutura de mapeamentodoesn’t handle cascading operations. Então - por exemplo - se acionarmossave em um dos pais, a criança não será salva automaticamente - precisaremos acionar explicitamente o salvamento na criança se quisermos salvá-la também.

É exatamente ondelife cycle events come in handy.

3. Eventos de Ciclo de Vida

Spring Data MongoDB publica alguns eventos de ciclo de vida muito úteis - comoonBeforeConvert, onBeforeSave, onAfterSave, onAfterLoadeonAfterConvert.

Para interceptar um dos eventos, precisamos registrar uma subclasse deAbstractMappingEventListenere substituir um dos métodos aqui. Quando o evento é despachado, nosso ouvinte será chamado e o objeto de domínio transmitido.

3.1. Salvar Cascade Básico

Vejamos o exemplo que tínhamos anteriormente - salvandouser comemailAddress. Agora podemos ouvir o eventoonBeforeConvert que será chamado antes de um objeto de domínio entrar no conversor:

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

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


Agora só precisamos registrar o ouvinte emMongoConfig:

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

Ou como XML:

E nós temos a semântica em cascata pronta, embora apenas para o usuário.

3.2. Uma Implementação Genérica de Cascade

Vamos agora melhorar a solução anterior emmaking the cascade functionality generic. Vamos começar definindo uma anotação personalizada:

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

Vamos agorawork on our custom listener para lidar com esses campos genericamente e não ter que lançar para qualquer entidade particular:

public class CascadeSaveMongoEventListener extends AbstractMongoEventListener {

    @Autowired
    private MongoOperations mongoOperations;

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


Portanto, estamos usando o utilitário de reflexão do Spring e executando nosso retorno de chamada em todos os campos que atendem aos nossos critérios:

@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 você pode ver, estamos procurando campos que tenham a anotaçãoDBRef e tambémCascadeSave. Depois de encontrarmos esses campos, salvamos a entidade filha.

Vejamos a classeFieldCallback que estamos usando para verificar se a criança tem uma anotação@Id:

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;
    }
}

Por fim, para que tudo funcione em conjunto, é claro que precisamos que o campoemailAddress agora seja anotado corretamente:

@DBRef
@CascadeSave
private EmailAddress emailAddress;

3.3. O Teste Cascade

Vamos agora dar uma olhada em um cenário - salvamos umUser comemailAddress, e a operação de salvamento se desdobra para esta entidade incorporada automaticamente:

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

Vamos verificar nosso banco de dados:

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

4. Conclusão

Neste artigo, ilustramos alguns recursos interessantes do Spring Data MongoDB - a anotação@DBRef, eventos de ciclo de vida e como podemos lidar com a cascata de forma inteligente.

A implementação de todos esses exemplos e trechos de códigocan be found over on GitHub - este é um projeto baseado em Maven, portanto, deve ser fácil de importar e executar como está.