1. Introduction
In this follow-up to our introduction to Couchbase, we create a set of Spring services that can be used together to create a basic persistence layer for a Spring application without the use of Spring Data.
2. Cluster Service
In order to satisfy the constraint that only a single CouchbaseEnvironment may be active in the JVM, we begin by writing a service that connects to a Couchbase cluster and provides access to data buckets without directly exposing either the Cluster or CouchbaseEnvironment instances.
2.1. Interface
Here is our ClusterService interface:
public interface ClusterService { Bucket openBucket(String name, String password); }
2.2. Implementation
Our implementation class instantiates a DefaultCouchbaseEnvironment and connects to a cluster during the @PostConstruct phase during Spring context initialization.
This ensures that the cluster is not null and that it is connected when the class is injected into other service classes, thus enabling them to open one or more data buckets:
@Service public class ClusterServiceImpl implements ClusterService { private Cluster cluster; @PostConstruct private void init() { CouchbaseEnvironment env = DefaultCouchbaseEnvironment.create(); cluster = CouchbaseCluster.create(env, "localhost"); } ... }
Next, we provide a ConcurrentHashMap to contain the open buckets and implement the openBucket method:
private Map<String, Bucket> buckets = new ConcurrentHashMap<>(); @Override synchronized public Bucket openBucket(String name, String password) { if(!buckets.containsKey(name)) { Bucket bucket = cluster.openBucket(name, password); buckets.put(name, bucket); } return buckets.get(name); }
3. Bucket Service
Depending on how you architect your application, you may need to provide access to the same data bucket in multiple Spring services.
If we merely attempted to open the same bucket in two or more services during application startup, the second service to attempt this is likely to encounter a ConcurrentTimeoutException.
To avoid this scenario, we define a BucketService interface and an implementation class per bucket. Each implementation class acts as a bridge between the ClusterService and the classes that need direct access to a particular Bucket.
3.1. Interface
Here is our BucketService interface:
public interface BucketService { Bucket getBucket(); }
3.2. Implementation
The following class provides access to the “baeldung-tutorial” bucket:
@Service @Qualifier("TutorialBucketService") public class TutorialBucketService implements BucketService { @Autowired private ClusterService couchbase; private Bucket bucket; @PostConstruct private void init() { bucket = couchbase.openBucket("baeldung-tutorial", ""); } @Override public Bucket getBucket() { return bucket; } }
By injecting the ClusterService in our TutorialBucketService implementation class and opening the bucket in a method annotated with @PostConstruct, we have ensured that the bucket will be ready for use when the TutorialBucketService is then injected into other services.
4. Persistence Layer
Now that we have a service in place to obtain a Bucket instance, we will create a repository-like persistence layer that provides CRUD operations for entity classes to other services without exposing the Bucket instance to them.
4.1. The Person Entity
Here is the Person entity class that we wish to persist:
public class Person { private String id; private String type; private String name; private String homeTown; // standard getters and setters }
4.2. Converting Entity Classes To and From JSON
To convert entity classes to and from the JsonDocument objects that Couchbase uses in its persistence operations, we define the JsonDocumentConverter interface:
public interface JsonDocumentConverter<T> { JsonDocument toDocument(T t); T fromDocument(JsonDocument doc); }
4.3. Implementing the JSON Converter
Next, we need to implement a JsonConverter for Person entities.
@Service public class PersonDocumentConverter implements JsonDocumentConverter<Person> { ... }
We could use the Jackson library in conjunction with the JsonObject class’s toJson and fromJson methods to serialize and deserialize the entities, however there is additional overhead in doing so.
Instead, for the toDocument method, we’ll use the fluent methods of the JsonObject class to create and populate a JsonObject before wrapping it a JsonDocument:
@Override public JsonDocument toDocument(Person p) { JsonObject content = JsonObject.empty() .put("type", "Person") .put("name", p.getName()) .put("homeTown", p.getHomeTown()); return JsonDocument.create(p.getId(), content); }
And for the fromDocument method, we’ll use theJsonObject class’s getString method along with the setters in the Person class in our fromDocument method:
@Override public Person fromDocument(JsonDocument doc) { JsonObject content = doc.content(); Person p = new Person(); p.setId(doc.id()); p.setType("Person"); p.setName(content.getString("name")); p.setHomeTown(content.getString("homeTown")); return p; }
4.4. CRUD Interface
We now create a generic CrudService interface that defines persistence operations for entity classes:
public interface CrudService<T> { void create(T t); T read(String id); T readFromReplica(String id); void update(T t); void delete(String id); boolean exists(String id); }
4.5. Implementing the CRUD Service
With the entity and converter classes in place, we now implement the CrudService for the Person entity, injecting the bucket service and document converter shown above and retrieving the bucket during initialization:
@Service public class PersonCrudService implements CrudService<Person> { @Autowired private TutorialBucketService bucketService; @Autowired private PersonDocumentConverter converter; private Bucket bucket; @PostConstruct private void init() { bucket = bucketService.getBucket(); } @Override public void create(Person person) { if(person.getId() == null) { person.setId(UUID.randomUUID().toString()); } JsonDocument document = converter.toDocument(person); bucket.insert(document); } @Override public Person read(String id) { JsonDocument doc = bucket.get(id); return (doc != null ? converter.fromDocument(doc) : null); } @Override public Person readFromReplica(String id) { List<JsonDocument> docs = bucket.getFromReplica(id, ReplicaMode.FIRST); return (docs.isEmpty() ? null : converter.fromDocument(docs.get(0))); } @Override public void update(Person person) { JsonDocument document = converter.toDocument(person); bucket.upsert(document); } @Override public void delete(String id) { bucket.remove(id); } @Override public boolean exists(String id) { return bucket.exists(id); } }
5. Putting it All Together
Now that we have all of the pieces of our persistence layer in place, here’s a simple example of a registration service that uses the PersonCrudService to persist and retrieve registrants:
@Service public class RegistrationService { @Autowired private PersonCrudService crud; public void registerNewPerson(String name, String homeTown) { Person person = new Person(); person.setName(name); person.setHomeTown(homeTown); crud.create(person); } public Person findRegistrant(String id) { try{ return crud.read(id); } catch(CouchbaseException e) { return crud.readFromReplica(id); } } }
6. Conclusion
We have shown that with a few basic Spring services, it is fairly trivial to incorporate Couchbase into a Spring application and implement a basic persistence layer without using Spring Data.
The source code shown in this tutorial is available in the github project.
You can learn more about the Couchbase Java SDK at the official Couchbase developer documentation site.