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.
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 …
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?
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…
Why wouldn’t refresItem not work without equals/hashCode? I don’t think one needs any “refresh*” methods in this kind of setup Edit: nor the generated id.
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.