MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Cascade personnalisée dans Spring Data MongoDB

1. Présentation

Ce didacticiel continuera à explorer certaines des fonctionnalités principales de Spring Data MongoDB - le @DBRef annotations et événements du cycle de vie.

2. @DBRef

Le cadre de mappage ne prend pas en charge le stockage des relations parent-enfant et des documents intégrés dans d'autres documents. Ce que nous pouvons faire, c'est que nous pouvons les stocker séparément et utiliser un DBRef pour se référer aux documents.

Lorsque l'objet est chargé à partir de MongoDB, ces références seront résolues avec impatience et nous récupérerons un objet mappé qui a le même aspect que s'il avait été stocké dans notre document maître.

Regardons un peu de code :

@DBRef
private EmailAddress emailAddress;

Adresse e-mail ressemble à :

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

Notez que le cadre de mappage ne gère pas les opérations en cascade . Ainsi, par exemple, si nous déclenchons une sauvegarde sur un parent, l'enfant ne sera pas enregistré automatiquement - nous devrons déclencher explicitement l'enregistrement sur l'enfant si nous voulons également l'enregistrer.

C'est précisément là que les événements du cycle de vie sont utiles .

3. Événements du cycle de vie

Spring Data MongoDB publie des événements de cycle de vie très utiles - tels que onBeforeConvert, onBeforeSave, onAfterSave, onAfterLoad et onAfterConvert.

Pour intercepter l'un des événements, nous devons enregistrer une sous-classe de AbstractMappingEventListener et remplacer l'une des méthodes ici. Lorsque l'événement est envoyé, notre écouteur sera appelé et l'objet de domaine transmis.

3.1. Enregistrement en cascade de base

Regardons l'exemple que nous avions plus tôt - enregistrer l'utilisateur avec l'adresseemail . Nous pouvons maintenant écouter le onBeforeConvert événement qui sera appelé avant qu'un objet de domaine n'entre dans le convertisseur :

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

Il ne nous reste plus qu'à enregistrer l'écouteur dans MongoConfig :

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

Ou en XML :

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

Et nous avons une sémantique en cascade entièrement réalisée, mais uniquement pour l'utilisateur.

3.2. Une implémentation générique en cascade

Améliorons maintenant la solution précédente en rendant la fonctionnalité de cascade générique. Commençons par définir une annotation personnalisée :

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

Passons maintenant à travailler sur notre écouteur personnalisé pour gérer ces champs de manière générique et ne pas avoir à caster vers une entité particulière :

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

Nous utilisons donc l'utilitaire de réflexion de Spring et nous exécutons notre rappel sur tous les champs qui répondent à nos critères :

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

Comme vous pouvez le voir, nous recherchons des champs qui ont à la fois la DBRef annotation ainsi que CascadeSave . Une fois que nous avons trouvé ces champs, nous enregistrons l'entité enfant.

Regardons le FieldCallback classe que nous utilisons pour vérifier si l'enfant a un @Id annotation :

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

Enfin, pour que tout fonctionne ensemble, nous avons bien sûr besoin de emailAddress le champ soit maintenant correctement annoté :

@DBRef
@CascadeSave
private EmailAddress emailAddress;

3.3. Le test en cascade

Jetons maintenant un coup d'œil à un scénario - nous sauvons un utilisateur avec adresseemail , et l'opération d'enregistrement se répercute automatiquement sur cette entité intégrée :

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

Vérifions notre base de données :

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