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.

Course – LSD (cat=Persistence)

Get started with Spring Data JPA through the reference Learn Spring Data JPA course:

>> CHECK OUT THE COURSE
res – Persistence (eBook) (cat=Persistence)
Comments are closed on this article!