databaseoraclejavafxlisteneruisearchbar

TableView stops getting filtered by search bar after updating data


I'm trying to create a search bar with a listener to perform real-time searching and filtering of data in javafx. The dataSearch method should be triggered when I click the execute button that starts the program. Initially, my search bar seems to be working fine, but the issue is that when I update my Oracle's database from the application, the TableView stops getting filtered by the input keyword from the search bar. It seems that the dataSearch method is only invoked once every time the program is executed, and after the TableView is modified, the ObservableList called generalList doesn't automatically get updated.

Code example:

public class App extends Application {
    private TableView<General> generalTableView;
    private ObservableList<General> generalList;
    private TextField searchBox;
    private TextField addTextField;

    @Override
    public void start(Stage primaryStage) {
        generalTableView = new TableView<>();
        TableColumn<General, String> generalColumn = new TableColumn<>("Col");
        generalColumn.setCellValueFactory(new PropertyValueFactory<>("col"));
        generalTableView.getColumns().add(generalColumn);

        Button loadDataButton = new Button("Load Data");
        loadDataButton.setOnAction(event -> {
            createGeneralTable();
            loadGeneralData();
            dataSearch();
        });

        searchBox = new TextField();
        searchBox.setPromptText("Search");

        addTextField = new TextField();
        addTextField.setPromptText("Add Data");

        Button addButton = new Button("Add");
        addButton.setOnAction(event -> addData(addTextField.getText()));

        HBox searchBoxContainer = new HBox(searchBox);
        HBox addBoxContainer = new HBox(addTextField, addButton);

        VBox root = new VBox(generalTableView, loadDataButton, searchBoxContainer, addBoxContainer);

        Scene scene = new Scene(root, 400, 400);
        primaryStage.setScene(scene);
        primaryStage.show();

        loadDataButton.fire(); // Trigger the button action when the program starts
    }

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

    static class General {
        private ObjectProperty<String> col;

        public General(String col) {
            this.col = new SimpleObjectProperty<>(col);
        }

        public String getCol() {
            return col.get();
        }

        public void setCol(String col) {
            this.col.set(col);
        }

        public ObjectProperty<String> colProperty() {
            return col;
        }
    }

    static class OracleConnect {
        public static Connection getConnection() {
            Connection connection = null;
            try {
                Class.forName("oracle.jdbc.driver.OracleDriver");
                connection = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:XE", "system", "o4a75e");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return connection;
        }
    }

    private void createGeneralTable() {
        try {
            Connection connection = OracleConnect.getConnection();
            String createTableGeneral = "CREATE TABLE general (col VARCHAR2(50))";
            PreparedStatement statement = connection.prepareStatement(createTableGeneral);
            statement.execute();

            statement.close();
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private void loadGeneralData() {
        try {
            Connection connection = OracleConnect.getConnection();
            String query = "SELECT col FROM general";
            PreparedStatement statement = connection.prepareStatement(query);
            ResultSet resultSet = statement.executeQuery();
            generalList = FXCollections.observableArrayList();

            while (resultSet.next()) {
                String col = resultSet.getString("col");
                General general = new General(col);
                generalList.add(general);
            }

            generalTableView.setItems(generalList);

            resultSet.close();
            statement.close();
            connection.close();

              
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private void dataSearch() {
        ObservableList<General> generalList = FXCollections.observableArrayList(generalTableView.getItems());

        FilteredList<General> filteredData = new FilteredList<>(generalList, b -> true);

        searchBox.textProperty().addListener((observable, oldValue, newValue) -> {
            filteredData.setPredicate(general -> {
                if (newValue.isEmpty() || newValue.isBlank() || newValue == null) {
                    return true;
                }

                String searchKeyword = newValue.toLowerCase();
                if (general.getCol().toLowerCase().contains(searchKeyword)) {
                    return true;
                } else {
                    return false;
                }
            });
        });

        SortedList<General> sortedData = new SortedList<>(filteredData);
        sortedData.comparatorProperty().bind(generalTableView.comparatorProperty());

        generalTableView.setItems(sortedData);
    }

    private void addData(String data) {
        try {
            Connection connection = OracleConnect.getConnection();
            String insertQuery ="INSERT INTO general (col) VALUES (?)";
            PreparedStatement statement = connection.prepareStatement(insertQuery);
            statement.setString(1, data);
            statement.executeUpdate();

            General general = new General(data);
            generalList.add(general);

            statement.close();
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Solution

  • This answer is for filtering data which is added to the in-memory list backing a TableView, so no database code is included.

    The main problem you have in the code in your question is that you are creating new lists of data unnecessarily. Instead, just have a single list of data and use that everywhere, wrapping it in a single FilteredList and a single SortedList for the required transformations on the underlying list data.

    A good way to enforce that you don't create more instances of objects and collections than are needed, is to declare the references as final, which is the technique demonstrated in the example code.

    Example: Sorted, Filtered TableView

    Cat.java

    Create a record to hold your data.

    package com.example.cats;
    
    record Cat(String name) {}
    

    IronCatFistClan.java

    Create a model to hold your records, sorting and filtering them.

    You could just simply call it Model rather than IronCatFistClan.

    package com.example.cats;
    
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.collections.transformation.FilteredList;
    import javafx.collections.transformation.SortedList;
    
    class IronCatFistClan {
        private final ObservableList<Cat> cats = FXCollections.observableArrayList(
                new Cat("Grizabella"),
                new Cat("Bombalurina"),
                new Cat("Rum Tum Tugger")
        );
    
        private final FilteredList<Cat> filteredCats = new FilteredList<>(
                cats
        );
        private final SortedList<Cat> sortedFilteredCats = new SortedList<>(
                filteredCats
        );
    
        public ObservableList<Cat> getCats() {
            return cats;
        }
    
        public SortedList<Cat> getSortedFilteredCats() {
            return sortedFilteredCats;
        }
    
        public void setSearchText(String searchText) {
            filteredCats.setPredicate(
                    cat ->
                            containsCaseInsensitive(
                                    cat.name(),
                                    searchText
                            )
            );
        }
    
        private static boolean containsCaseInsensitive(String textToSearch, String searchText) {
            if (searchText == null || searchText.isEmpty() || searchText.isBlank()) {
                return true;
            }
    
            return textToSearch.toLowerCase().contains(
                    searchText.toLowerCase()
            );
        }
    }
    

    FilteredCatApp.java

    Create a UI that interacts with the model, displaying a table view of the data, with a search function that sets a filter on the data and an add function that adds a new record to the data.

    package com.example.cats;
    
    import javafx.application.Application;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.geometry.Insets;
    import javafx.scene.Scene;
    import javafx.scene.control.*;
    import javafx.scene.layout.*;
    import javafx.stage.Stage;
    
    public class FilteredCatApp extends Application {
        @Override
        public void start(Stage primaryStage) {
            IronCatFistClan ironCatFistClan = new IronCatFistClan();
    
            VBox root = new VBox(
                    10,
                    createTable(ironCatFistClan),
                    createSearchBox(ironCatFistClan),
                    createAddControls(ironCatFistClan)
            );
            root.setPadding(new Insets(10));
    
            Scene scene = new Scene(root, 400, 400);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        private static HBox createAddControls(IronCatFistClan ironCatFistClan) {
            TextField newCatName = new TextField();
            newCatName.setPromptText("Name of a cat to add");
    
            Button addButton = new Button("Add Cat");
            addButton.setDefaultButton(true);
            addButton.setOnAction(event ->
                    ironCatFistClan.getCats().add(
                            new Cat(newCatName.getText())
                    )
            );
    
            return new HBox(10, newCatName, addButton);
        }
    
        private static TextField createSearchBox(IronCatFistClan ironCatFistClan) {
            TextField searchBox = new TextField();
    
            searchBox.setPromptText("Search");
            searchBox.textProperty().addListener((observable, oldSearchText, newSearchText) ->
                    ironCatFistClan.setSearchText(newSearchText)
            );
    
            return searchBox;
        }
    
        private static TableView<Cat> createTable(IronCatFistClan ironCatFistClan) {
            TableView<Cat> tableView = new TableView<>(
                    ironCatFistClan.getSortedFilteredCats()
            );
    
            ironCatFistClan.getSortedFilteredCats().comparatorProperty().bind(
                    tableView.comparatorProperty()
            );
    
            TableColumn<Cat, String> nameColumn = new TableColumn<>("Name");
            nameColumn.setCellValueFactory(p ->
                    new SimpleStringProperty(p.getValue().name())
            );
            tableView.getColumns().add(nameColumn);
    
            return tableView;
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    FAQ

    Should I create a listener to track the changes in the database's table and execute the search method whenever a change occurs?

    No. I don't recommend that.

    Perhaps you could create a database trigger to monitor data changes. The trigger could invoke Java code (I don't know how to do that) to notify that the data in the database has changed. The Java code can query the new data in the database and notify listeners of the change event. However, that seems like a kind of xy problem.

    The data backing a TableView is an in-memory replica of data in the database. It can get out of sync in the event of concurrent database updates. One way to keep the data in sync is to have the database notify and push changes to the application (like the trigger concept sketched out above).

    However, the more common approach is to retrieve the current data from the database every time you want to update the UI based on new data. The application pulls the current data from the database when needed, instead of trying to get the database to publish some event that data has changed.

    This might suggest that the search method, which contains a listener, is only executed once as I click the button that runs the program.

    No. Your method dataSearch() is invoked from your method loadGeneralData which is executed in the onAction event handler of the your loadDataButton. You fire that button on app startup, but the method can also be executed if the user manually presses the button.

    Every time you call dataSearch() you are adding a new listener to your search text. You probably don't want to do that. Each time you press the loadDataButton new listeners will be added and the old ones will not be removed, and they will end up retaining references to the old data lists, which is kind of buggy. You should at least remove old listeners before adding new ones to retain that approach.

    A better solution is a kind of MVC, where you interact with an observable data model, updating aspects of it like the data in the model, or filters and sorting order as needed (as demonstrated in the example in this answer).

    If necessary (and not shown here), transactional persistence services and data access objects can be added to the application to interface the model code with the database as needed.