CRUD with MicroStream/EclipseStore

I want to use MicroStream/EclipseStore as a persistence layer for my Vaadin 24.3.5 app.
I have a Vaadin CRUD for a simple DTO. When I click the Edit button, the CRUD editor opens and I can edit the selected DTO. After saving, the changed value is also displayed properly with the changed values in the row of the CRUD grid. So far so good.
However, if I now click on the Edit button a second time, the CRUD Editor opens, but remains empty. No DTO data is displayed. I suspect some problem with the ID of the DTO after the first SAVE by MicroStream/EclipseStore. The usual suspects methods equals and hashCode are implemented IMO appropriately. Attached is an minimal realiable example MRE, which reproduces the problem.

What needs to be changed to make MicroStream/EclipseStore work with a Vaadin CRUD ?

@PageTitle("MRE")
@Route(value = "mre", layout = MainLayout.class)
@PermitAll
public class ExampleView extends VerticalLayout {

    // this is the DTO the CRUD should show
    @Data
    @ToString
    public static class Customer {
        private String firstname;
        private String lastname;

        // both constructors are needed

        public Customer() {
        }

        // needed by the crud.newListener
        public Customer(Crud.NewEvent<Customer> customerNewEvent) {
        }

        // vaadin grid needs to identify the items by equals/hashCode
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;

            if (o == null || getClass() != o.getClass()) return false;

            Customer customer = (Customer) o;

            return new EqualsBuilder().append(firstname, customer.firstname).append(lastname, customer.lastname).isEquals();
        }

        @Override
        public int hashCode() {
            return new HashCodeBuilder(17, 37).append(firstname).append(lastname).toHashCode();
        }
    }

    @Data
    @ToString
    static class Root {
        private List<Customer> customers;
    }

    // spring config for the storage manager
    // using name/qualifier in order to work with your Vaadin app this example is embedded in and if those Vaadin app already has
    // their existing storageManager
    @Configuration
    static class Config {

        @Bean(destroyMethod = "shutdown", name = "myStorageManager")
        public StorageManager defineStorageManager() {

            NioFileSystem fileSystem     = NioFileSystem.New();

            // https://docs.microstream.one/manual/storage/customizing/custom-class-loader.html
            EmbeddedStorageManager storageManager = EmbeddedStorageFoundation.New()
                .onConnectionFoundation(cf ->
                    cf.setClassLoaderProvider(ClassLoaderProvider.New(
                        Thread.currentThread().getContextClassLoader()
                    ))
                )
                .setConfiguration(
                    StorageConfiguration.Builder()
                        .setStorageFileProvider(
                            Storage.FileProviderBuilder(fileSystem)
                                .setDirectory(fileSystem.ensureDirectoryPath("mre/data"))
                                .createFileProvider()
                        )
                        .setChannelCountProvider(StorageChannelCountProvider.New(2))
                        .createConfiguration()
                )
                .createEmbeddedStorageManager();

            storageManager.start();

            Root root = (Root) storageManager.root();
            if (root == null) {
                root = new Root();
                root.setCustomers(new ArrayList<>());
            } else {
                System.out.println(root.toString());
            }

            storageManager.setRoot(root);
            storageManager.storeRoot();

            return storageManager;
        }
    }

    // UI
    private Crud<Example.Customer> crud;
    private TextField firstname;
    private TextField lastname;

    // deps
    private CustomerService customerService;

    public Example(CustomerService customerService) {
        this.customerService = customerService;

        this.crud = new Crud<>(Customer.class, createEditor());
        setupGrid();
        setupDataProvider();

        add(crud);
    }

    private void setupGrid() {
        CrudGrid<Example.Customer> grid = new CrudGrid<>(Example.Customer.class, false);
        this.crud.setGrid(grid);
    }

    private void setupDataProvider() {
        List<Customer> customers = customerService.getAll();

        // simple ListDataProvider not compatible for Crud
        // com.vaadin.flow.component.crud.CrudFilter cannot be cast to class com.vaadin.flow.function.SerializablePredicate
        DataProvider<Customer, Void> dataProvider = new CallbackDataProvider<>(
            fetch -> { fetch.getLimit(); fetch.getOffset(); return customers.stream(); },
            size -> customers.size());

        crud.setDataProvider(dataProvider);
        crud.addDeleteListener(this::delete);
        crud.addSaveListener(this::save);
        crud.addNewListener(Customer::new);
    }

    private void save(Crud.SaveEvent<Customer> customerSaveEvent) {
        customerService.save(customerSaveEvent.getItem());
        this.crud.getGrid().getDataProvider().refreshAll();
    }

    private void delete(Crud.DeleteEvent<Customer> customerDeleteEvent) {
        customerService.delete(customerDeleteEvent.getItem());
        this.crud.getGrid().getDataProvider().refreshAll();
    }

    private CrudEditor<Customer> createEditor() {
        Binder<Customer> binder = new Binder<>(Customer.class);
        binder.bindInstanceFields(this);

        FormLayout layout = new FormLayout();
        layout.setWidthFull();
        firstname.setWidthFull();
        lastname.setWidthFull();

        layout.setResponsiveSteps(new FormLayout.ResponsiveStep("1px",1));
        layout.addFormItem(firstname, "Firstname");
        layout.addFormItem(lastname, "Lastname");

        BinderCrudEditor<Customer> editor= new BinderCrudEditor<>(binder, layout);
        return editor;
    }

    // ##################################
    @Service
    static class CustomerService {


        private final StorageManager storageManager;

        public CustomerService(@Qualifier("myStorageManager") StorageManager storage) {
            this.storageManager = storage;
        }

        private Root root() {
            return (Root) storageManager.root();
        }

        public List<Customer> getAll() {
            Root root = root();
            root.getCustomers().sort(Comparator.comparing(Customer::getLastname));
            return root.getCustomers();
        }

        public void save(Customer item) {
            if (root().getCustomers().contains(item)) {
                storageManager.store(item);
            } else {
                root().getCustomers().add(item);
                storageManager.store(root().getCustomers());
            }
        }

        public void delete(Customer item) {
            root().getCustomers().remove(item);
            storageManager.store(root().getCustomers());
        }
    }
}

Your equals and hash code don’t look good to me. If the CrudGrid, is based on Vaadin Grid, which uses HashMap internally, you can’t use mutable fields in equals/hashCode.

Or then you need to reset the list once you have saved the entity.

I have had a more in-depth article about this half ready for a half a year, maybe I should push myself to get that finished…

Hi Matti,
thanks for your response. As long as MicroStream doesn’t really need an ID for the entities, my DTO doesn’t has such an immutable field. I try to add such an immutable field and modify the equals/hashCode accordingly …

Why do you need equals and hashCode ?

I have learnt in the past that it is better to have these methods for a grid DTO. Otherwise use cases like selection, inline-edit (GridPro) etc. did not work properly.

But BTW … isn’t there an API for providing something like an ID provider method?

You mean this: Vaadin Tip: Lazy Loading and Item Identity - Learn to Code with Simon Martinelli

The default should be (with or without Vaadin) not to override equals/hashCode. That is needed if you word with e.g. detached entities.

I checked out your code. Replace @Data annotation with @Getter + @Setter and remove equals/hashCode implementations and you are fine. I’ll try to come up with a more thorough answer (and my related article) at some point, bu too many things on the table now…

But without equals/hashCode refreshItem will not work.

yeah … thats what I recognized too …

with

a) remove all lombok helpers
b) stay with an -generated- ID like

public Customer() {
    this.id = UUID.randomUUID().toString();
}

it finally works.

Why wouldn’t refresItem not work without equals/hashCode? I don’t think one needs any “refresh*” methods in this kind of setup :thinking: Edit: nor the generated id.

Tested quickly and to me this varioation of the example seems to work fine:

package com.example.demo;

import com.vaadin.flow.component.crud.BinderCrudEditor;
import com.vaadin.flow.component.crud.Crud;
import com.vaadin.flow.component.crud.CrudEditor;
import com.vaadin.flow.component.crud.CrudGrid;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.provider.DataProvider;
import com.vaadin.flow.router.Route;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.eclipse.serializer.reflect.ClassLoaderProvider;
import org.eclipse.store.afs.nio.types.NioFileSystem;
import org.eclipse.store.storage.embedded.types.EmbeddedStorageFoundation;
import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
import org.eclipse.store.storage.types.Storage;
import org.eclipse.store.storage.types.StorageChannelCountProvider;
import org.eclipse.store.storage.types.StorageConfiguration;
import org.eclipse.store.storage.types.StorageManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

@Route(value = "mre")
public class ExampleView extends VerticalLayout {

    // this is the DTO the CRUD should show
    @Getter
    @Setter
    @ToString
    public static class Customer {
        private String firstname;
        private String lastname;

        public Customer() {
        }

    }

    @Getter
    @Setter
    static class Root {
        private List<Customer> customers;
    }

    // spring config for the storage manager
    // using name/qualifier in order to work with your Vaadin app this example is embedded in and if those Vaadin app already has
    // their existing storageManager
    @Configuration
    static class Config {

        @Bean(destroyMethod = "shutdown", name = "myStorageManager")
        public StorageManager defineStorageManager() {

            NioFileSystem fileSystem     = NioFileSystem.New();

            // https://docs.microstream.one/manual/storage/customizing/custom-class-loader.html
            EmbeddedStorageManager storageManager = EmbeddedStorageFoundation.New()
                .onConnectionFoundation(cf ->
                    cf.setClassLoaderProvider(ClassLoaderProvider.New(
                        Thread.currentThread().getContextClassLoader()
                    ))
                )
                .setConfiguration(
                    StorageConfiguration.Builder()
                        .setStorageFileProvider(
                            Storage.FileProviderBuilder(fileSystem)
                                .setDirectory(fileSystem.ensureDirectoryPath("mre/data"))
                                .createFileProvider()
                        )
                        .setChannelCountProvider(StorageChannelCountProvider.New(2))
                        .createConfiguration()
                )
                .createEmbeddedStorageManager();

            storageManager.start();

            Root root = (Root) storageManager.root();
            if (root == null) {
                root = new Root();
                root.setCustomers(new ArrayList<>());
            } else {
                System.out.println(root.toString());
            }

            storageManager.setRoot(root);
            storageManager.storeRoot();

            return storageManager;
        }
    }

    // UI
    private Crud<Customer> crud;
    private TextField firstname;
    private TextField lastname;

    // deps
    private CustomerService customerService;

    public ExampleView(CustomerService customerService) {
        this.customerService = customerService;

        this.crud = new Crud<>(Customer.class, createEditor());
        setupCrud();
        setupDataProvider();

        add(crud);
    }

    private void setupCrud() {
        CrudGrid<Customer> grid = new CrudGrid<>(Customer.class, false);
        this.crud.setGrid(grid);
        crud.addDeleteListener(this::delete);
        crud.addSaveListener(this::save);
        crud.addNewListener(e -> new Customer());
    }

    private void setupDataProvider() {
        List<Customer> customers = customerService.getAll();
        // simple ListDataProvider not compatible for Crud
        // com.vaadin.flow.component.crud.CrudFilter cannot be cast to class com.vaadin.flow.function.SerializablePredicate
        crud.setDataProvider(DataProvider.fromCallbacks(
                query -> customers.stream()
                        .skip(query.getOffset())
                        .limit(query.getLimit()),
                size -> customers.size())
        );
    }

    private void save(Crud.SaveEvent<Customer> customerSaveEvent) {
        customerService.save(customerSaveEvent.getItem());
    }

    private void delete(Crud.DeleteEvent<Customer> customerDeleteEvent) {
        customerService.delete(customerDeleteEvent.getItem());
    }

    private CrudEditor<Customer> createEditor() {
        Binder<Customer> binder = new Binder<>(Customer.class);
        binder.bindInstanceFields(this);

        FormLayout layout = new FormLayout();
        layout.setWidthFull();
        firstname.setWidthFull();
        lastname.setWidthFull();

        layout.setResponsiveSteps(new FormLayout.ResponsiveStep("1px",1));
        layout.addFormItem(firstname, "Firstname");
        layout.addFormItem(lastname, "Lastname");

        BinderCrudEditor<Customer> editor= new BinderCrudEditor<>(binder, layout);
        return editor;
    }

    // ##################################
    @Service
    static class CustomerService {


        private final StorageManager storageManager;

        public CustomerService(@Qualifier("myStorageManager") StorageManager storage) {
            this.storageManager = storage;
        }

        private Root root() {
            return (Root) storageManager.root();
        }

        public List<Customer> getAll() {
            Root root = root();
            root.getCustomers().sort(Comparator.comparing(Customer::getLastname));
            return root.getCustomers();
        }

        public void save(Customer item) {
            if (root().getCustomers().contains(item)) {
                storageManager.store(item);
            } else {
                root().getCustomers().add(item);
                storageManager.store(root().getCustomers());
            }
        }

        public void delete(Customer item) {
            root().getCustomers().remove(item);
            storageManager.store(root().getCustomers());
        }
    }
}

BTW. There is now the spring boot module available for MicroStream. Didn’t yet test it myself anywhere, but probably cleans up the required configuration a bit.

One “note on the subject”, easy to miss if moving from e.g. JPA based backend where all our objects are essentially “snapshots of the DB state” or “detached objects”. With this kind of architecture if running EclipseStore on the same JVM, those might be “the real thing” if not using DTOs of any kind.

That makes things super fast and easy to get things done, but bit depending on the app and data, “unexpected things” may happen. For example if somebody is editing your data, but din’t yet push save, the state might have been changed for somebody else. Tested that it doesn’t happen with this Crud component, this probably uses “buffered binding”, but just as a warning. For this reason, depending the requirements again, it might sometimes be good to create those DTOs, or at least use something like BeanUtils (IIRC there was a way to do that with Lombok as well) to artficially make those objects “detached entities” that you pass to the UI layer.

1 Like