6

Short Question: What is the best way to populate a Java FX table view with data from a database by using a record? (rather than a typical object class with getters and setters)

Full question: I have this InventoryRecord.java file that I recently created to replace a traditional Object called "Inventory" that I used to populate a tableview:

import java.util.List;

public record InventoryRecord(
        String make
        , String model
        , String officialModel
        , String upc
        , List<String> category
        , String type
        , String description
        ,String vendor
        , Double cost
        , Double price
        ) {}

I would like to use this record as my framework to populate my TableView using Data from a Database (in this case its Neo4j but that's not important)

Before I had this InventoryRecord record, I was using a typical class called Inventory that looked like this (I have shortened it of course to keep the question readable, assume there are the getters/setters for each property of Inventory.)

public class Inventory {
    private String make;
    private String model;
    private String officialModel;
    private List<String> category;
    private String type;
    private String description;
    private String vendor;
    private Double cost;
    private Double price;
    private String upc;
    //Method for actual instantiation of an Inventory Object, uses setter methods in order to perform validations for each field
    public Inventory(String make, String model, String officialModel, String upc, List<String> category, String type, String description,String vendor, Double cost, Double price) {
        this.setMake(make);
        this.setModel(model);
        this.setOfficialModel(officialModel);
        this.setUpc(upc);
        this.setCategory(category);
        this.setType(type);
        this.setDescription(description);
        this.setVendor(vendor);
        this.setCost(cost);
        this.setPrice(price);
    }

And this was how I populated the table with the query results

while (result.hasNext()) {
                //result.next() serves as our iterator that increments us through the records and through the while loop
                Record record = result.next();
                
                String make = record.get("make").asString();
                String model = record.get("model").asString();
                String officialModel = record.get("officialModel").asString();
                String upc = record.get("upc").asString();                
                List<String> category = record.get("categories").asList(Value::asString);
                String type = record.get("type").asString();
                String desc = record.get("description").asString();
                String vendor = record.get("vendor").asString();
                Double cost = record.get("cost").isNull() ? null : record.get("cost").asDouble();
                Double price = record.get("price").isNull() ? null : record.get("price").asDouble();
                
                //when creating the anonymous Inventory items, you can send them to the constructor with any name
                inventoryTable.getItems().add(new Inventory(make, model, officialModel,upc, category, type, desc,vendor, cost, price));
            }
            
            //The property value factory must use the property names as string arguments in the constructor
            makeColumn.setCellValueFactory(new PropertyValueFactory<>("make"));
            modelColumn.setCellValueFactory(new PropertyValueFactory<>("model"));
            officialModelColumn.setCellValueFactory(new PropertyValueFactory<>("officialModel"));
            categoryColumn.setCellValueFactory(new PropertyValueFactory<>("category"));
            upcColumn.setCellValueFactory(new PropertyValueFactory<>("upc"));
            typeColumn.setCellValueFactory(new PropertyValueFactory<>("type"));
            descriptionColumn.setCellValueFactory(new PropertyValueFactory<>("description"));
            vendorColumn.setCellValueFactory(new PropertyValueFactory<>("vendor"));
            costColumn.setCellValueFactory(new PropertyValueFactory<>("cost"));
            priceColumn.setCellValueFactory(new PropertyValueFactory<>("price"));
            
            inventoryTable.setItems(inventoryTable.getItems());

But how can I switch everything to a InventoryRecord rather than just the Inventory object, the .setCellValueFactory and PropertyValueFactory don't like what they see. I tried this (with some help from Google Bard) and it seems to not give any errorsfor the strings: makeColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().make())); But for the List and Double properties/columns I tried this:

categoryColumn.setCellValueFactory(cellData -> new SimpleListProperty<List<String>>(cellData.getValue().category()));
costColumn.setCellValueFactory(cellData -> new SimpleDoubleProperty(cellData.getValue().cost()));

and I get these errors: Type mismatch: cannot convert from SimpleListProperty<List<String>> to ObservableValue<List<String>> Type mismatch: cannot convert from SimpleDoubleProperty to ObservableValue<Double>

Any help is much, much appreciated!

I tried this:

while (result.hasNext()) {
                //result.next() serves as our iterator that increments us through the records and through the while loop
                Record record = result.next();
                
                String make = record.get("make").asString();
                String model = record.get("model").asString();
                String officialModel = record.get("officialModel").asString();
                String upc = record.get("upc").asString();                
                List<String> category = record.get("categories").asList(Value::asString);
                String type = record.get("type").asString();
                String desc = record.get("description").asString();
                String vendor = record.get("vendor").asString();
                Double cost = record.get("cost").isNull() ? null : record.get("cost").asDouble();
                Double price = record.get("price").isNull() ? null : record.get("price").asDouble();
                
                // Create an InventoryRecord directly from the Record
                InventoryRecord inventoryRecord = new InventoryRecord(make,model,officialModel,upc,category,type,desc,vendor,cost,price);
                // Add the InventoryRecord to the table items
                inventoryTable.getItems().add(inventoryRecord);
            }
            
            //The property value factory must use the property names as string arguments in the constructor
            makeColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().make()));
            modelColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().model()));
            officialModelColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().officialModel()));
            categoryColumn.setCellValueFactory(cellData -> new SimpleListProperty<List<String>>(cellData.getValue().category()));
            upcColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().upc()));
            typeColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().type()));
            descriptionColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().description()));
            vendorColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().vendor()));
            costColumn.setCellValueFactory(cellData -> new SimpleDoubleProperty(cellData.getValue().cost()));
            priceColumn.setCellValueFactory(cellData -> new SimpleDoubleProperty(cellData.getValue().price()));
            
            inventoryTable.setItems(inventoryTable.getItems());

but got these errors: Type mismatch: cannot convert from SimpleListProperty<List<String>> to ObservableValue<List<String>> Type mismatch: cannot convert from SimpleDoubleProperty to ObservableValue<Double>

I expect something like this (This is how it worked with my Inventory class, the one I'm trying to replace with InventoryRecord): TableView using Inventory Class

1

2 Answers 2

5

The PropertyValueFactory class will not work with records, at least not as of JavaFX 21, but you've already figured that out (I recommend avoiding that class regardless). The errors you're getting are not directly related to you changing the class to a record. They're caused by returning the wrong type in a few of your cell-value factories.

A SimpleListProperty<E> is already a list of E. By parameterizing it with List<String> you are saying you have a list of lists of string. And based on the error you're getting, that's not what the column's value type is. Return a SimpleObjectProperty<List<String>> from the appropriate cell value factory instead.

Additionally, a SimpleDoubleProperty is an ObservableValue<Number>. But from the error you're getting, you actually need to return an ObservableValue<Double>. Change the appropraite cell value factories to return a SimpleObjectProperty<Double> instead. The other option is to change the column's value type from Double to Number. I recommend the former approach, however.

To simplify this somewhat, you can create a utility method in your class. Something like:

private <T> void setCellValueFactory(
    TableColumn<InventoryRecord, T> column, Callback<InventoryRecord, T> callback) {
  column.setCellValueFactory(
      data -> {
        T value = callback.call(data.getValue());
        return new SimpleObjectProperty<>(value);
      });
}

Which would let you define all the cell value factories like so:

setCellValueFactory(makeColumn, InventoryRecord::make);
setCellValueFactory(modelColumn, InventoryRecord::model);
setCellValueFactory(officialModelColumn, InventoryRecord::officialModel);
setCellValueFactory(categoryColumn, InventoryRecord::category);
setCellValueFactory(upcColumn, InventoryRecord::upc);
setCellValueFactory(typeColumn, InventoryRecord::type);
setCellValueFactory(descriptionColumn, InventoryRecord::description);
setCellValueFactory(vendorColumn, InventoryRecord::vendor);
setCellValueFactory(costColumn, InventoryRecord::cost);
setCellValueFactory(priceColumn, InventoryRecord::price);
Sign up to request clarification or add additional context in comments.

Comments

3

Thanks @Slaw for the very elegant solution and explanation. I reached a slightly different solution since in my research I realized I was not aware of what JavaFX beans was.😅 The main solution was me just setting real JavaFX getters and setters that use beans "property" methods in my actual Inventory Constructor.

This is what my constructor ended up looking like (shortened) for the various data types:

public class Inventory {
  private StringProperty officialModel;
  private ListProperty<String> category;
  private DoubleProperty cost;

    public final String getOfficialModel() {
        return this.officialModelProperty().get();
    }
    

    public final void setOfficialModel(final String officialModel) {
        this.officialModelProperty().set(officialModel);
    }
    

    public final ListProperty<String> categoryProperty() {
        if (category == null) {
            category = new SimpleListProperty<String>(this, "category");
        }
        return category;
    }
    

    public final ObservableList<String> getCategory() {
        return this.categoryProperty().get();
    }
    

    public final void setCategory(final ObservableList<String> category) {
        this.categoryProperty().set(category);
    }
public final DoubleProperty costProperty() {
        if (cost == null) {
            cost = new SimpleDoubleProperty(this, "cost");
        }
        return cost;
    }
    

    public final double getCost() {
        return this.costProperty().get();
    }
    

    public final void setCost(final double cost) {
        this.costProperty().set(cost);
    }

And my CellValueFactory looks like this:

makeColumn.setCellValueFactory(cellData -> cellData.getValue().makeProperty());
modelColumn.setCellValueFactory(cellData -> cellData.getValue().modelProperty());
officialModelColumn.setCellValueFactory(cellData -> cellData.getValue().officialModelProperty());
//categoryColumn must be read only, so it can't be mutated by user, which protects the list data
categoryColumn.setCellValueFactory(cellData -> new ReadOnlyObjectWrapper<>(cellData.getValue().categoryProperty().get()));
upcColumn.setCellValueFactory(cellData -> cellData.getValue().upcProperty());
typeColumn.setCellValueFactory(cellData -> cellData.getValue().typeProperty());
descriptionColumn.setCellValueFactory(cellData -> cellData.getValue().descriptionProperty());
vendorColumn.setCellValueFactory(cellData -> cellData.getValue().vendorProperty());
costColumn.setCellValueFactory(cellData -> cellData.getValue().costProperty().asObject());  // Wrap double as Object for ObservableValue
priceColumn.setCellValueFactory(cellData -> cellData.getValue().priceProperty().asObject()); // Wrap double as Object for ObservableValue

Let me know if this solution has any major flaws though, if it does I may switch to your approach!

1 Comment

Yes, this is the preferred approach when using JavaFX's TableView and similar. Though the verbosity is unfortunate. Note you look to be using the "fully lazy" approach, which is more complicated and may not provide much benefit in your case. That said, if you want or need to use a record with TableView, then using JavaFX properties in the model is discouraged; it does not make much sense to make the components of a Java record be JavaFX properties (not least because records should generally be immutable).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.