β’ A Simple Tagging Implementation with JPA
β’ An Advanced Tagging Implementation with JPA
1. Overview
In this tutorial, weβll take a look at a simple tagging implementation using Java and MongoDB.
For those unfamiliar with the concept, a tag is a keyword used as a βlabelβ to group documents into different categories. This allows the users to quickly navigate through similar content and itβs especially useful when dealing with a big amount of data.
That being said, itβs not surprising that this technique is very commonly used in blogs. In this scenario, each post has one or more tags according to the topics covered. When the user finishes reading, he can follow one of the tags to view more content related to that topic.
Letβs see how we can implement this scenario.
2. Dependency
In order to query the database, weβll have to include the MongoDB driver dependency in our pom.xml:
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>3.6.3</version>
</dependency>
The current version of this dependency can be found here.
3. Data Model
First of all, letβs start by planning out what a post document should look like.
To keep it simple, our data model will only have a title, which weβll also use as the document id, an author, and some tags.
Weβll store the tags inside an array since a post will probably have more than just one:
{
"_id" : "Java 8 and MongoDB",
"author" : "Donato Rimenti",
"tags" : ["Java", "MongoDB", "Java 8", "Stream API"]
}
Weβll also create the corresponding Java model class:
public class Post {
private String title;
private String author;
private List<String> tags;
// getters and setters
}
4. Updating Tags
Now that we have set up the database and inserted a couple of sample posts, letβs see how we can update them.
Our repository class will include two methods to handle the addition and removal of tags by using the title to find them. Weβll also return a boolean to indicate whether the query updated an element or not:
public boolean addTags(String title, List<String> tags) {
UpdateResult result = collection.updateOne(
new BasicDBObject(DBCollection.ID_FIELD_NAME, title),
Updates.addEachToSet(TAGS_FIELD, tags));
return result.getModifiedCount() == 1;
}
public boolean removeTags(String title, List<String> tags) {
UpdateResult result = collection.updateOne(
new BasicDBObject(DBCollection.ID_FIELD_NAME, title),
Updates.pullAll(TAGS_FIELD, tags));
return result.getModifiedCount() == 1;
}
We used the addEachToSet method instead of push for the addition so that if the tags are already there, we wonβt add them again.
Notice also that the addToSet operator wouldnβt work either since it would add the new tags as a nested array which is not what we want.
Another way we can perform our updates is through the Mongo shell. For instance, letβs update the post JUnit5 with Java. In particular, we want to add the tags Java and JUnit5 and remove the tags Spring and REST:
db.posts.updateOne(
{ _id : "JUnit 5 with Java" },
{ $addToSet :
{ "tags" :
{ $each : ["Java", "JUnit5"] }
}
});
db.posts.updateOne(
{_id : "JUnit 5 with Java" },
{ $pull :
{ "tags" : { $in : ["Spring", "REST"] }
}
});
5. Queries
Last but not least, letβs go through some of the most common queries we may be interested in while working with tags. For this purpose, weβll take advantage of three array operators in particular:
- $in β returns the documents where a field contains any value of the specified array
- $nin β returns the documents where a field doesnβt contain any value of the specified array
- $all β returns the documents where a field contains all the values of the specified array
Weβll define three methods to query the posts in relation to a collection of tags passed as arguments. They will return the posts which match at least one tag, all the tags and none of the tags. Weβll also create a mapping method to handle the conversion between a document and our model using Java 8βs Stream API:
public List<Post> postsWithAtLeastOneTag(String... tags) {
FindIterable<Document> results = collection
.find(Filters.in(TAGS_FIELD, tags));
return StreamSupport.stream(results.spliterator(), false)
.map(TagRepository::documentToPost)
.collect(Collectors.toList());
}
public List<Post> postsWithAllTags(String... tags) {
FindIterable<Document> results = collection
.find(Filters.all(TAGS_FIELD, tags));
return StreamSupport.stream(results.spliterator(), false)
.map(TagRepository::documentToPost)
.collect(Collectors.toList());
}
public List<Post> postsWithoutTags(String... tags) {
FindIterable<Document> results = collection
.find(Filters.nin(TAGS_FIELD, tags));
return StreamSupport.stream(results.spliterator(), false)
.map(TagRepository::documentToPost)
.collect(Collectors.toList());
}
private static Post documentToPost(Document document) {
Post post = new Post();
post.setTitle(document.getString(DBCollection.ID_FIELD_NAME));
post.setAuthor(document.getString("author"));
post.setTags((List<String>) document.get(TAGS_FIELD));
return post;
}
Again, letβs also take a look at the shell equivalent queries. Weβll fetch three different post collection respectively tagged with MongoDB or Stream API, tagged with both Java 8 and JUnit 5 and not tagged with Groovy nor Scala:
db.posts.find({
"tags" : { $in : ["MongoDB", "Stream API" ] }
});
db.posts.find({
"tags" : { $all : ["Java 8", "JUnit 5" ] }
});
db.posts.find({
"tags" : { $nin : ["Groovy", "Scala" ] }
});
6. Conclusion
In this article, we showed how to build a tagging mechanism. Of course, we can use and readapt this same methodology for other purposes apart from a blog.
If you are interested further in learning MongoDB, we encourage you to read this introductory article.
