Last week while I was at JFokus I met Matti Tahvonen, he works at Vaadin. They have been proposing an open source web framework for rich Internet applications in Java for years and do it really well. I am personally really happy to write a full web modern application just in Java.
We took 10 minutes to have a working Vaadin CRUD sample storing object in Couchbase. The result is available on Github. Since then I also migrated a JPA based sample also available here. You can see how very little work it requires and how easy it is to go from JPA to Couchbase with the diff.
Spring Data Couchbase meet Vaadin
Generate the Project
The first step when starting a Spring project is to go on Spring Initializr. Here you can select the version and dependencies you want for your project. Select Spring Boot version 1.4.0(SNAPSHOT) and add Vaadin and Couchbase as dependencies.
Now you can generate the project and import it as a Maven project in your editor of choice.
Basic Person Entity CRUD
This CRUD sample is going to store Customer objects. A customer has an id, a firstName and a lastName. Also, the last name must not be null. To express this as an entity, you just have to add the @Document annotation on the class, @Id annotation on the field to be used as Couchbase key, generate getters and setters and you are done. To express the not null constraints, we can simply use the Java validation annotations @NotNull. To make sure it's picked up when writing the entity, we'll need to declare a validator bean.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
package hello; import java.util.Objects; import java.util.UUID; import javax.validation.constraints.NotNull; import org.springframework.data.couchbase.core.mapping.Document; import com.couchbase.client.java.repository.annotation.Id; @Document public class Customer { @Id private String id = UUID.randomUUID().toString(); private String firstName; @NotNull private String lastName; protected Customer() { } public Customer(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } @Override public String toString() { return String.format("Customer[id=%s, firstName='%s', lastName='%s']", id, firstName, lastName); } @Override public int hashCode() { int hash = 7; hash = 37 * hash + Objects.hashCode(this.id); return hash; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Customer other = (Customer) obj; return Objects.equals(this.id, other.id); } } |
The Customer Repository
Once you have define an entity, you need to create the associated repository. Create an interface that extends the CouchbasePagingAndSortingRepository. This repository handles Customer entities with a String as key.
I have redefined the findAll method to return a List instead of an Iterable as it plays better with Vaadin structures. The findAll method is backed up by a View. To have your views defined automatically you can add the @ViewIndexed annotation. You also need to make sure you have set the spring.data.couchbase.auto-index property to true in your application.properties file.
I also added a findByLastName(String lastName) method. Based on the method name, the appropriate N1QL query will be automatically generated. But to execute N1Ql query, you need a primary index. Which can also be generated automatically through the @N1QLPrimaryIndexed annotation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package hello; import java.util.List; import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed; import org.springframework.data.couchbase.core.query.ViewIndexed; import org.springframework.data.couchbase.repository.CouchbasePagingAndSortingRepository; @ViewIndexed(designDoc = "customer") @N1qlPrimaryIndexed public interface CustomerRepository extends CouchbasePagingAndSortingRepository<Customer, String> { List<Customer> findAll(); List<Customer> findByLastName(String lastName); } |
Configuration
I am using spring spring-boot-starter-data-couchbase. It provides autoconfiguration. This autoconfiguration can be activated by setting the spring.couchbase.bootstrap-hosts property. So far my application.properties looks like this:
1 2 3 |
spring.couchbase.bootstrap-hosts=localhost spring.data.couchbase.auto-index=true |
Create a Customer
Now I have everything I need to save a Customer Entity in Couchbase. We can try this easily with a CommandLineRunner:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@SpringBootApplication public class Application { private static final Logger log = LoggerFactory.getLogger(Application.class); public static void main(String[] args) { SpringApplication.run(Application.class); } @Bean public CommandLineRunner loadData(CustomerRepository repository) { return (args) -> { repository.save(new Customer(null, "Dessler")); }; } @Bean public Validator validator() { return new LocalValidatorFactoryBean(); } } |
You'll notice that I have also added the validator bean previsouly mentionned. It uses a ValidatingCouchbaseEventListener declared automatically by the spring boot auto-config.
Using Vaadin for UI
The backend is ready, we can start thinking about the frontend. I want a basic CRUD app that will show a list of Customer, the ability to add, edit or remove elements of the list. Here's a screenshot:
To Build this we start by creating a form that allow the user to enter a first name and a last name. Create a class that extends an AbstractForm of Customer. This class is not available in Vaadin Core, so we need to add Viritin.
Viritin is a server side enhancement library for Vaadin. It fixes some bad defaults in the core framework and provides more fluent and intelligent API for existing components. It also provides several major enhancements to databinding and provides completely new components made with server side composition (no widgetset is needed).
And yes it provides the AbstractForm class that is neatly integrated with Spring Data and validators. We need to edit the firstName and lastName fields of the Customer class, so we define two text fields called firstName and lastName. They have to have the same name as your Customer field. What is also great about this component is that it will pickup the validation annotation on your entity. This way you get automatic validation on the client and on the server. And it supports more complex annotations thanl@NotNull like @Size or @Pattern.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package hello; import org.vaadin.viritin.form.AbstractForm; import org.vaadin.viritin.layouts.MFormLayout; import com.vaadin.spring.annotation.SpringComponent; import com.vaadin.spring.annotation.UIScope; import com.vaadin.ui.Component; import com.vaadin.ui.TextField; @SpringComponent @UIScope public class CustomerEditor extends AbstractForm<Customer> { /* Fields to edit properties in Customer entity */ TextField firstName = new TextField("First name"); TextField lastName = new TextField("Last name"); public CustomerEditor() { setVisible(false); } @Override protected Component createContent() { return new MFormLayout(firstName, lastName, getToolbar()); } } |
Now that the form is ready we can build the full UI displaying the table grid. This will be the main component of your Vaadin application, the main web page. Since the CustomerRepository and the CustomerEditor are Spring beans, we can inject them direclty in the constructor. If you are familiar with writing Java UI, the commented code below should be straight forward.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
package hello; import org.springframework.beans.factory.annotation.Autowired; import org.vaadin.viritin.fields.MTable; import org.vaadin.viritin.layouts.MVerticalLayout; import com.vaadin.annotations.Theme; import com.vaadin.server.FontAwesome; import com.vaadin.server.VaadinRequest; import com.vaadin.spring.annotation.SpringUI; import com.vaadin.ui.Button; import com.vaadin.ui.UI; @SpringUI @Theme("valo") public class VaadinUI extends UI { private final CustomerRepository repo; private final CustomerEditor editor; private final MTable<Customer> grid; private final Button addNewBtn; @Autowired public VaadinUI(CustomerRepository repo, CustomerEditor editor) { this.repo = repo; this.editor = editor; this.grid = new MTable<>(Customer.class).withProperties("id", "firstName", "lastName").withHeight("300px"); this.addNewBtn = new Button("New customer", FontAwesome.PLUS); } @Override protected void init(VaadinRequest request) { // Connect selected Customer to editor or hide if none is selected grid.addMValueChangeListener(e -> { if (e.getValue() == null) { editor.setVisible(false); } else { editor.setEntity(e.getValue()); } }); // Instantiate and edit new Customer the new button is clicked addNewBtn.addClickListener(e -> editor.setEntity(new Customer("", ""))); // Listen changes made by the editor, refresh data from backend editor.setSavedHandler(customer -> { repo.save(customer); listCustomers(); editor.setVisible(false); }); editor.setResetHandler(customer -> { editor.setVisible(false); listCustomers(); }); editor.setDeleteHandler(customer -> { repo.delete(customer); listCustomers(); }); // Initialize listing listCustomers(); // build layout setContent(new MVerticalLayout(addNewBtn, grid, editor)); } private void listCustomers() { grid.setBeans(repo.findAll()); } } |
And then you are all set, all you have to do is run this as a usual Java Spring Boot application. So that was pretty easy huh?