4.2.2 Mapping domain types to documents
Spring Data MongoDB offers a handful of annotations that are useful for mapping domain types to document structures to be persisted in MongoDB. Although Spring Data MongoDB provides a half-dozen annotations for mapping, only the following four are useful for most common use cases:
@Id—— Designates a property as the document ID (from Spring Data Commons)@Document—— Declares a domain type as a document to be persisted to MongoDB@Field—— Specifies the field name (and, optionally, the order) for storing a property in the persisted document@Transient—— Specifies that a property is not to be persisted
Of those three annotations, only the @Id and @Document annotations are strictly required. Unless you specify otherwise, properties that aren’t annotated with @Field or @Transient will assume a field name equal to the property name.
Applying these annotations to the Ingredient class, you get the following:
package tacos;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Document
@AllArgsConstructor
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
public class Ingredient {
@Id
private String id;
private String name;
private Type type;
public static enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}As you can see, you place the @Document annotation at the class level to indicate that Ingredient is a document entity that can be written to and read from a Mongo database. By default, the collection name (the Mongo analog to a relational database table) is based on the class name, with the first letter lowercase. Because you haven’t specified otherwise, Ingredient objects will be persisted to a collection named ingredient. But you can change that by setting the collection attribute of @Document as follows:
@Data
@AllArgsConstructor
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@Document(collection="ingredients")
public class Ingredient {
...
}You’ll also notice that the id property has been annotated with @Id. This designates the property as being the ID of the persisted document. You can use @Id on any property whose type is Serializable, including String and Long. In this case, you’re already using the String-defined id property as a natural identifier, so there’s no need to change it to any other type.
So far, so good. But you’ll recall from earlier in this chapter that Ingredient was the easy domain type to map for Cassandra. The other domain types, such as Taco, were a bit more challenging. Let’s look at how you can map the Taco class to see what surprises it might hold.
MongoDB’s approach to document persistence lends itself very well to the domaindriven-design way of applying persistence at the aggregate root level. Documents in MongoDB tend to be defined as aggregate roots, with members of the aggregate as subdocuments.
What that means for Taco Cloud is that because Taco is only ever persisted as a member of the TacoOrder-rooted aggregate, the Taco class doesn’t need to be annotated as a @Document, nor does it need an @Id property. The Taco class can remain clean of any persistence annotations, as shown here:
package tacos;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;
@Data
public class Taco {
@NotNull
@Size(min=5, message="Name must be at least 5 characters long")
private String name;
private Date createdAt = new Date();
@Size(min=1, message="You must choose at least 1 ingredient")
private List<Ingredient> ingredients = new ArrayList<>();
public void addIngredient(Ingredient ingredient) {
this.ingredients.add(ingredient);
}
}The TacoOrder class, however, being the root of the aggregate, will need to be annotated with @Document and have an @Id property, as follows:
package tacos;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.CreditCardNumber;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.Data;
@Data
@Document
public class TacoOrder implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private String id;
private Date placedAt = new Date();
// other properties omitted for brevity's sake
private List<Taco> tacos = new ArrayList<>();
public void addTaco(Taco taco) {
this.tacos.add(taco);
}
}The TacoOrder class, however, being the root of the aggregate, will need to be annotated with @Document and have an @Id property, as follows:
@Data
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@RequiredArgsConstructor
@Document
public class User implements UserDetails {
private static final long serialVersionUID = 1L;
@Id
private String id;
private final String username;
private final String password;
private final String fullname;
private final String street;
private final String city;
private final String state;
private final String zip;
private final String phoneNumber;
// UserDetails method omitted for brevity's sake
}For brevity’s sake, I’ve snipped out the various delivery and credit card fields. But from what’s left, it’s clear that all you need is @Document and @Id, as with the other domain types.
Notice, however, that the id property has been changed to be a String (as opposed to a Long in the JPA version or a UUID in the Cassandra version). As I said earlier, @Id can be applied to any Serializable type. But if you choose to use a String property as the ID, you get the benefit of Mongo automatically assigning a value to it when it’s saved (assuming that it’s null). By choosing String, you get a databasemanaged ID assignment and needn’t worry about setting that property manually.
Although there are some more-advanced and unusual use cases that require additional mapping, you’ll find that for most cases, @Document and @Id, along with an occasional @Field or @Transient, are sufficient for MongoDB mapping. They certainly do the job for the Taco Cloud domain types.
All that’s left is to write the repository interfaces.
