Is it necessary to create classes for every single database query in order to work with TableView?
Asked Answered
U

2

0

In the last few days I went through some JavaFX tutorials explaining 'How to use TableViews?' Every single tutorial creates DataModel-Objects like Person for example. This is fine as long as you just want sql-statements like select Firstname, Lastname from Person.

But what if you don't want to create data models for every single select-query, because you join data with another objects for whatever reason? For example, select * from Person join City join Country. What I learned the last days is, if you create a tableview containing only List (representing the rows), it becomes horrible to handle formatting or even evaluating the tableview.

Is that really a javaFX thing? Or did I miss something?

Update

In order to make it more clear, see this question. I first found this question after reading answers and comments to my question, but I am not satisfied with the answer to the linked question.

Uniformize answered 18/5, 2021 at 13:11 Comment(2)
Do you have a particular use case that you're talking about? It is typical for a UI control in an application to display the results of a particular query. It is not common to design a UI control to display the results of some arbitrary query.Shreve
Perhaps this is a duplicate of: Create a dynamic tableview with generic types.Abigael
R
3

Your question is mixing up two concepts here: javafx.scene.control.TableView on one hand, and SQL/ORM on the other hand. Let's forget about SQL queries and such as I think your concern is mostly on TableView.

TableView must be populated with a List. The inner type of this list must match the TableView's generic type. So for instance TableView<Person> will be populated with a List<Person>.

Other than that, the actual type of the objects representing rows can be anything. It doesn't even have to contain the data itself, or it can very-well be, for instance, a Map<String, Object>. In this case, you will map the keys of the row to each column by defining a CellValueFactory for each column, that will return the entry value for your chosen key.

Then you can convert this value to a String and/or a Nodeby defining a CellFactory for the column.

Objects with JavaFX properties can be mapped more easily, as there exists pre-made PropertyValueFactory that will require only the property name. But they're not the only way to go.

Example using a Map<String, Object> for each row:

import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javafx.application.Application;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class App extends Application {

  private TableView<Map<String, Object>> tableView;
  private TableColumn<Map<String, Object>, String> nameCol;
  private TableColumn<Map<String, Object>, Integer> ageCol;
  
  private Stage stage;

  @Override
  public void start(Stage stage) {
    this.stage = stage;

    //Insert a TableView into scene
    this.tableView = new TableView<>();
    this.nameCol = new TableColumn<>("Name");
    this.nameCol.setPrefWidth(250);
    ageCol = new TableColumn<>("Age");
    tableView.getColumns().add(nameCol);
    tableView.getColumns().add(ageCol);
    
    nameCol.setCellValueFactory(param -> {
      Map<String, Object> v = param.getValue();
      return new SimpleStringProperty(v == null ? null : (String) v.get("name"));
    });
    
    ageCol.setCellValueFactory(param -> {
      Map<String, Object> v = param.getValue();
      return new SimpleObjectProperty<Integer>(v == null ? null : (Integer) v.get("age"));
    });
    ageCol.setCellFactory(param -> {
      return new TableCell<>() {
        @Override
        public void updateItem(Integer item, boolean empty) {
          if (empty || item == null)
            super.setText("");
          else
            super.setText(NumberFormat.getIntegerInstance().format(item));
          //Could also call setGraphic(Node)
        }
      };
    });
    
    final Scene scene = new Scene(new BorderPane(tableView), 640, 480);

    stage.setScene(scene);
    stage.setOnShown(event -> stageReady()); //Continue when stage is rendered
    stage.show();
  }

  private void stageReady() {
    //Generate data
    List<Map<String, Object>> data = new ArrayList<>();
    Map<String, Object> obj1 = new HashMap<>();
    obj1.put("name", "Name of Object 1");
    obj1.put("age", 42);
    data.add(obj1);
    Map<String, Object> obj2 = new HashMap<>();
    obj2.put("name", "OBJECT 2");
    obj2.put("age", 53);
    data.add(obj2);
    //Show data
    tableView.getItems().setAll(data);
  }

  public static void main(String[] args) {
    launch();
  }

}

ageCol.setCellFactory is only here for demonstration. If I had not set it, then the Integer cells would have been rendered calling Integer.toString() for the cells text property.

Rms answered 18/5, 2021 at 13:27 Comment(4)
Yeah correct. SQL is not the problem... Maybe my example wasn't well chosen. So the most common way is to use Collections(e.g. Map) to handle such mixed Entitys? Can I still use bindings with maps? For example I want to style the tableview depending on its cell contentUniformize
Collections, JsonObjects from json-simple, anythingRms
You can use bindings, but without JavaFX properties, they won't react to changes in your data objects. You can also use the TableCell produced by CellFactory to update styles in its updateItem methodRms
Your TableView also has a row factory that works in a similar way as a cell factory. I could go on all day long. If you want more guidance, ask a specific question.Rms
B
3

As shown here, a database query can be used to construct an ObservableList<Map> of attribute-value pairs. As shown in Adding Maps of Data to the Table, MapValueFactory is "A convenience implementation of the Callback interface," like PropertyValueFactory, "designed specifically for use within the TableColumn. As discussed here, the convenience comes at a cost.

Inspired by this related example, the variation below displays a TableView containing the entries contained in the Map<String,​String> returned by System.getenv(). Although that map is unmodifiable, simple editing support is shown.

image

import java.util.Map;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

/**
 * https://docs.oracle.com/javase/8/javafx/user-interface-tutorial/table-view.htm
 * https://mcmap.net/q/1427392/-binding-hashmap-with-tableview-javafx
 * https://mcmap.net/q/1477916/-is-it-necessary-to-create-classes-for-every-single-database-query-in-order-to-work-with-tableview
 * https://mcmap.net/q/912985/-jtable-duplicate-values-in-row
 */
public class TableMapSample extends Application {

    private final String columnKey = "Key";
    private final String columnValue = "Value";

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        ObservableList<Map.Entry<String, String>> list
            = FXCollections.observableArrayList(System.getenv().entrySet());
        TableView<Map.Entry<String, String>> table = new TableView<>(list);
        table.setEditable(true);
        table.getSelectionModel().setCellSelectionEnabled(true);

        TableColumn<Map.Entry<String, String>, String> keyColumn = new TableColumn<>(columnKey);
        keyColumn.setCellValueFactory((TableColumn.CellDataFeatures<Map.Entry<String, String>, String> p)
            -> new SimpleStringProperty(p.getValue().getKey()));
        table.getColumns().add(keyColumn);
        keyColumn.setCellFactory(TextFieldTableCell.forTableColumn());

        TableColumn<Map.Entry<String, String>, String> valueColumn = new TableColumn<>(columnValue);
        valueColumn.setCellValueFactory((TableColumn.CellDataFeatures<Map.Entry<String, String>, String> p)
            -> new SimpleStringProperty(p.getValue().getValue()));
        valueColumn.setCellFactory(TextFieldTableCell.forTableColumn());
        valueColumn.setPrefWidth(800);
        table.getColumns().add(valueColumn);

        stage.setTitle("Table Map.Entry<K,V>");
        stage.setScene(new Scene(new StackPane(table)));
        stage.show();
    }
}
Burmese answered 30/10, 2021 at 18:39 Comment(1)
See also Why should I avoid using PropertyValueFactory in JavaFX?.Burmese

© 2022 - 2024 — McMap. All rights reserved.