Pension Management App with JavaFX
Build a desktop pension management app with JavaFX, JDBC, and PostgreSQL.
Introduction
30 Second Summary
Retirement pension management involves tracking who gets paid, how much, and what happens when someone passes away. Getting this wrong means surviving spouses miss out on benefits they are entitled to.
In this project, you will build a JavaFX desktop application that manages retirement pensions, automatically calculates amounts based on diplomas, and transfers 40% of the pension to a surviving spouse when a pensionnaire is marked as deceased.
What You'll Build
When you launch your finished app, you see a full table of pensionnaires, add a new one with the pension amount auto-calculated, mark someone as deceased, and watch their spouse automatically appear in the survivor table with the correct transferred amount.
By the end of this project, you'll have:
- A working pension form where you add a pensionnaire, select their diploma, and the montant auto-fills from the TARIF table.
- An automatic survivor transfer where marking a married pensionnaire as deceased creates a new entry in the CONJOINT table with 40% of the original pension amount.
- A search and filter system using SQL LIKE queries with partial matching and a status filter that displays live totals.
- Secret Mission: A BarChart histogram showing the distribution of pensionnaires by diploma category.
Are there any prerequisites?
This project assumes you already have JDK 26, Scene Builder 26.0.0, NetBeans IDE with Maven, and PostgreSQL with pgAdmin installed on your machine.
If you do not have these installed yet, set them up before starting Step 1.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
You're creating a pension management system that handles real business logic. When a pensionnaire is marked as deceased and they have a spouse, the application automatically transfers 40% of their pension to the surviving spouse. Think about why automating that rule matters instead of leaving it to manual entry.
Set Up the Maven JavaFX Project and Database
Before you can build any user interface or business logic, you need a properly configured project with all the right dependencies and a database ready to store pension data.
In this step, you will create a Maven JavaFX project in NetBeans, set up a PostgreSQL database with the pension schema, and wire up the JDBC connection between them.
In this step, get ready to:
- Create and configure the Maven JavaFX project in NetBeans with all dependencies.
- Create the PostgreSQL database and tables for the pension system.
- Set up the JDBC connection utility class.
Create the Maven JavaFX project in NetBeans
- Open NetBeans IDE.
- Click File in the top menu bar.
- Click New Project.
- In the New Project dialog, expand Java with Maven and select FXML JavaFX Maven Archetype (Gluon).
- Click Next.
- Enter gestion-pension as the project name.
- Set the Group ID to com.pension.
- Click Finish to create the project.
NetBeans generates a project with a default pom.xml and some starter files. You need to replace the pom.xml contents with your specific configuration.
- In the Projects panel on the left, expand gestion-pension and double-click pom.xml to open it.
- Select all existing content and delete it.
- Paste the following project header and properties at the top of pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.pension</groupId>
<artifactId>gestion-pension</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>26</maven.compiler.source>
<maven.compiler.target>26</maven.compiler.target>
<javafx.version>26.0.2</javafx.version>
</properties>
What does this configure?
- The groupId and artifactId identify your project in the Maven ecosystem.
- The javafx.version property centralizes the JavaFX version so all dependencies reference it.
- The compiler source and target are set to 26 to match your JDK.
- Directly below the properties section, paste the dependencies:
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.13</version>
</dependency>
</dependencies>
What are these dependencies?
- javafx-controls provides UI components like TableView, Button, and ComboBox.
- javafx-fxml enables loading layouts from FXML files designed in Scene Builder.
- postgresql is the JDBC driver that lets Java talk to your PostgreSQL database.
- Below the dependencies, paste the build plugin configuration and close the project tag:
<build>
<plugins>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<mainClass>com.pension/com.pension.App</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
What does the javafx-maven-plugin do?
This plugin lets you run your JavaFX app with mvn clean javafx:run. The mainClass uses the format modulename/fully.qualified.ClassName because your project is modular.
- Save pom.xml with Ctrl+S.
- NetBeans will prompt you to resolve dependencies. Click Resolve Problems or right-click the project and select Build with Dependencies to download the libraries.
You should see Maven download the JavaFX and PostgreSQL libraries in the Output panel. No errors means your pom.xml is correct.
Seeing dependency resolution errors?
- Check that your internet connection is active. Maven needs to download libraries from Maven Central.
- Verify there are no typos in the version numbers. The JavaFX version should be 26.0.2 and PostgreSQL driver 42.7.13.
Still stuck? Why can't Maven resolve my JavaFX or PostgreSQL dependencies?
Now you need the module descriptor and the application entry point. The archetype may have generated default versions, but you need to replace them with the project-specific ones.
- In the Projects panel, navigate to src/main/java and open module-info.java. Replace its entire contents with:
module com.pension {
requires javafx.controls;
requires javafx.fxml;
requires java.sql;
opens com.pension to javafx.fxml;
opens com.pension.controller to javafx.fxml;
opens com.pension.model to javafx.base;
exports com.pension;
}
Why do you need opens directives?
JavaFX uses reflection to inject @FXML fields into your controllers. The opens directives allow the JavaFX framework to access those packages at runtime. Without them, you get runtime errors when loading FXML files.
- Create the package com.pension under src/main/java if it does not already exist. Right-click src/main/java, select New then Java Package, and name it com.pension.
- Create App.java inside com.pension. Right-click the package, select New then Java Class, name it App, and replace the generated content with:
package com.pension;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class App extends Application {
@Override
public void start(Stage stage) throws Exception {
// Load the main view layout from FXML
Parent root = FXMLLoader.load(getClass().getResource("/com/pension/view/main-view.fxml"));
Scene scene = new Scene(root, 1000, 600);
stage.setTitle("Gestion Pension");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
What does App.java do?
This is your application entry point. It loads the main layout from an FXML file and displays a 1000x600 window titled "Gestion Pension". The FXML file will be designed in Scene Builder in the next step.
Since App.java loads main-view.fxml, you need to create a minimal placeholder so the app can launch.
- Create the directory structure src/main/resources/com/pension/view. Right-click src/main/resources, select New then Folder, and create the nested folders com/pension/view.
- Inside com/pension/view, create a new file named main-view.fxml and paste:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<BorderPane xmlns:fx="http://javafx.com/fxml" />
This placeholder creates an empty layout. You will design the real interface in Scene Builder in the next step.
- Save all files.
Before you run the app, what do you think you will see? A full interface, or just an empty window?
- Right-click the gestion-pension project in the Projects panel and select Run Maven then Goals.
- Type clean javafx:run in the Goals field and click OK.
A blank window titled "Gestion Pension" appears. That confirms your project structure, dependencies, and module configuration are all correct.
- Close the window to stop the app.
Window not appearing?
- If you see a NullPointerException, confirm that main-view.fxml is in src/main/resources/com/pension/view/. The path must match the string in App.java exactly.
- If you see "module com.pension does not export", verify your module-info.java has exports com.pension.
Help me fix my JavaFX module or FXML loading error.
Create the pension database in pgAdmin
Your application needs a database to store pensionnaire records, tariff rates, and spouse information. You will create the database and its four tables now.
- Open pgAdmin.
- In the left tree, expand Servers, then right-click Databases.
- Select Create then Database.
- In the General tab, enter pension_db as the Database name.
- Click Save.
The new pension_db appears in the tree under Databases. Now open the Query Tool to create the tables.
- Right-click pension_db in the tree and select Query Tool.
- Paste the following SQL to create the first two tables:
CREATE TABLE tarif (
num_tarif VARCHAR(20) PRIMARY KEY,
diplome VARCHAR(100) NOT NULL,
categorie VARCHAR(50) NOT NULL,
montant INTEGER NOT NULL
);
CREATE TABLE personne (
im VARCHAR(20) PRIMARY KEY,
nom VARCHAR(100) NOT NULL,
prenoms VARCHAR(100) NOT NULL,
datenais DATE NOT NULL,
diplome VARCHAR(100) NOT NULL,
contact VARCHAR(50),
statut BOOLEAN DEFAULT true,
situation VARCHAR(20),
nom_conjoint VARCHAR(100),
prenom_conjoint VARCHAR(100)
);
What are these tables?
- TARIF stores diploma categories and their associated pension amounts. This is the lookup table for auto-calculating pensions.
- PERSONNE stores each pensionnaire's personal information, including their marital status and spouse name.
- Click the Execute script button (or press F5) to run the SQL.
You should see "Query returned successfully" in the Messages panel. Now add the remaining tables and seed data.
- Clear the Query Tool editor and paste:
CREATE TABLE conjoint (
num_pension VARCHAR(30) PRIMARY KEY,
nom_conjoint VARCHAR(100) NOT NULL,
prenom_conjoint VARCHAR(100) NOT NULL,
montant INTEGER NOT NULL
);
CREATE TABLE payer (
im VARCHAR(20) REFERENCES personne(im),
num_tarif VARCHAR(20) REFERENCES tarif(num_tarif),
date_paiement DATE NOT NULL,
PRIMARY KEY (im, num_tarif, date_paiement)
);
INSERT INTO tarif (num_tarif, diplome, categorie, montant) VALUES
('T001', 'BACC', 'A', 150000),
('T002', 'LICENCE', 'B', 250000),
('T003', 'MASTER', 'C', 350000),
('T004', 'DOCTORAT', 'D', 500000);
What are these tables and data?
- CONJOINT stores spouse pension transfers. When a pensionnaire dies, their spouse's record goes here with 40% of the original pension.
- PAYER tracks payment history linking pensionnaires to their tariff.
- The INSERT seeds four tariff levels (BACC through DOCTORAT) so the form can calculate pensions automatically.
- Click Execute script (F5) again.
You should see "Query returned successfully" confirming all tables and seed data are created.
- In the tree on the left, expand pension_db then Schemas then public then Tables. You should see all four tables: conjoint, payer, personne, and tarif.
Now save this SQL as a reference file in your project. This is not used by the app at runtime, but keeps your schema documented alongside your code.
- Back in NetBeans, create a new file named schema.sql inside src/main/resources/com/pension/view/.
- Paste both SQL blocks you just executed (the full schema) and save the file.
Tables not showing in pgAdmin?
- Right-click Tables in the tree and click Refresh. pgAdmin sometimes caches the tree state.
- Make sure you ran the queries while connected to pension_db, not to the default postgres database.
Help me troubleshoot my pgAdmin table creation.
Create the database connection utility
Your Java code needs a way to connect to pension_db. A utility class centralizes the connection string so every DAO can share it.
- In NetBeans, right-click the com.pension package and create a new sub-package named com.pension.util.
- Inside com.pension.util, create a new Java class named DatabaseConnection and replace the content with:
package com.pension.util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DatabaseConnection {
// Connection parameters for the pension database
private static final String URL = "jdbc:postgresql://localhost:5432/pension_db";
private static final String USER = "postgres";
private static final String PASSWORD = "your-password-here";
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
}
What does this class do?
DriverManager.getConnection opens a live JDBC connection to your PostgreSQL database. Every time your app needs to read or write data, it calls DatabaseConnection.getConnection() to get a fresh connection.
Replace your-password-here with the actual password you set for the postgres user when you installed PostgreSQL.
- Save DatabaseConnection.java.
Before you run the app one last time, do you expect it to start successfully or throw a connection error? The app only loads the FXML layout at this point. It does not call DatabaseConnection yet.
- Run the app again: right-click the project, select Run Maven then Goals, and type clean javafx:run.
The blank "Gestion Pension" window appears again. Everything compiles cleanly, including the JDBC driver on the classpath and the new utility class. The database connection will be exercised in the next step when you load data into the TableView.
- Close the window.
Compilation error on DatabaseConnection?
- If you see "package java.sql is not visible", make sure module-info.java includes requires java.sql;.
- If you see "cannot find symbol: DriverManager", check your import statement matches java.sql.DriverManager exactly.
Why is my DatabaseConnection class not compiling?
✔️ Awesome, I've got everything!
Great. Double check you saved all files and that the window appeared with the title "Gestion Pension".
ⓧ I'd like to double check the full code
Scroll up and confirm each of these pieces is present and matches exactly:
- pom.xml with the three code chunks combined (header/properties, dependencies, build plugin).
- module-info.java with all requires and opens directives.
- App.java in com.pension loading /com/pension/view/main-view.fxml.
- main-view.fxml placeholder in src/main/resources/com/pension/view/.
- DatabaseConnection.java in com.pension.util.
- schema.sql saved in src/main/resources/com/pension/view/.
- All four tables visible in pgAdmin under pension_db.
Your project skeleton is ready with all dependencies, the database is seeded, and the connection utility is in place. Next up, you will design the main interface in Scene Builder so the TableView can display real pensionnaire data.
Design the Main Interface in Scene Builder
Your project runs and shows a blank window. That confirms your Maven dependencies and module configuration are solid.
Now you need a way to display and interact with pensionnaire data. In this step you will design the main interface using Scene Builder, then wire it to a controller and model so the TableView can bind directly to your data.
In this step, get ready to:
- Design the main layout (BorderPane with search bar, table, and buttons) in Scene Builder.
- Create the Personne model class with JavaFX properties.
- Create the MainController that configures each TableColumn.
Create the FXML file and open it in Scene Builder
FXML is an XML-based language that describes your user interface layout. Scene Builder generates this FXML automatically as you drag and drop components.
- In NetBeans, expand your project in the Projects panel.
- Navigate to src/main/resources/com/pension/view.
- Right-click the view folder, select New then Other.
- In the New File dialog, choose the JavaFX category and select Empty FXML.
- Name it main-view and click Finish.
- Right-click main-view.fxml in the Projects panel and select Open to launch Scene Builder.
Scene Builder does not open?
If the FXML opens in the text editor instead, you need to configure the Scene Builder path. Go to Tools then Options (or NetBeans then Preferences on macOS). Click the Java tab, then JavaFX. Set the Scene Builder Home path to your installation folder and click OK.
Help me configure Scene Builder in NetBeans
Design the BorderPane layout
A BorderPane divides the window into five regions: top, center, bottom, left, and right. You will use top for the search bar, center for the table, and bottom for the action buttons.
- In Scene Builder, open the Library panel on the left side.
- Delete the default root element (if present) in the Hierarchy panel at the bottom left.
- Under Containers in the Library, drag a BorderPane onto the canvas.
- Select the BorderPane and set its Pref Width to 1000 and Pref Height to 600 in the Layout section on the right.
Now build the top section with the search bar and filter controls:
- Drag an HBox from Containers into the TOP area of the BorderPane.
- Set the HBox Spacing to 10 and Padding to 10 on all sides in the Layout section.
- From Controls in the Library, drag a TextField into the HBox.
- Set the TextField Prompt Text to Rechercher par IM ou Nom in the Properties section.
- Drag a ComboBox into the HBox next to the TextField.
- Drag a Label into the HBox. Set its Text property to Effectif: 0.
Next, build the center section with the TableView:
- Drag a TableView from Controls into the CENTER area of the BorderPane.
- Select the TableView. In the right panel, remove any default columns that Scene Builder added.
- Add 9 TableColumn elements by right-clicking the TableView in the Hierarchy and selecting Add Column for each one.
- Click each column and set its Text property (in order): IM, Nom, Prenoms, Date Naissance, Diplome, Montant, Contact, Statut, Situation.
Finally, add the action buttons at the bottom:
- Drag an HBox into the BOTTOM area of the BorderPane.
- Set its Spacing to 10 and Padding to 10 on all sides.
- Drag three Button controls into this bottom HBox.
- Set the button text values to Ajouter, Modifier, and Supprimer.
What does the layout look like?
You should see a window with a search bar and filter at the top, a large table with 9 columns in the center, and three buttons along the bottom. This mirrors the structure of the pension management app you are building.
Set the fx:id values and controller class
The fx:id attribute links each visual element to a Java field in the controller. Without it, the controller cannot reference the UI component.
- Select the TextField in the top HBox. In the Code section (right panel), set fx:id to searchField.
- Select the ComboBox. Set its fx:id to filterStatut.
- Select the Label. Set its fx:id to labelEffectif.
- Select the TableView. Set its fx:id to tablePersonne.
- Select each column in order and set fx:id values: colIm, colNom, colPrenoms, colDatenais, colDiplome, colMontant, colContact, colStatut, colSituation.
- In the Hierarchy panel, select the root BorderPane. In the Code section, find the Controller class field. Type com.pension.controller.MainController.
- Save the file in Scene Builder by pressing Ctrl+S (Windows) or Cmd+S (macOS).
Why set a controller class here?
When FXMLLoader loads this FXML at runtime, it reads the controller class attribute and instantiates that class. It then injects each UI element into the matching @FXML field using the fx:id you just set.
Create the Personne model class
The TableView needs a data model to bind to. Each property in the model maps to a column in the table. JavaFX uses special property classes (like SimpleStringProperty) that support observable binding.
- In NetBeans, right-click the com.pension source package and create a new package called model.
- Inside com.pension.model, create a new Java class file named Personne.java.
- Add the package declaration, imports, and field declarations by pasting this code:
package com.pension.model;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
public class Personne {
// JavaFX properties for TableView binding
private final SimpleStringProperty im;
private final SimpleStringProperty nom;
private final SimpleStringProperty prenoms;
private final SimpleStringProperty datenais;
private final SimpleStringProperty diplome;
private final SimpleStringProperty contact;
private final SimpleBooleanProperty statut;
private final SimpleStringProperty situation;
private final SimpleStringProperty nomConjoint;
private final SimpleStringProperty prenomConjoint;
private final SimpleIntegerProperty montant;
}
Why JavaFX property types instead of plain fields?
Plain String or int fields cannot notify the UI when their value changes. SimpleStringProperty and SimpleIntegerProperty are ObservableList-compatible wrappers. The TableView listens to these properties and refreshes cells automatically when data changes.
- Add the constructor below the field declarations. It initializes each property:
public Personne(String im, String nom, String prenoms, String datenais,
String diplome, String contact, boolean statut,
String situation, String nomConjoint, String prenomConjoint, int montant) {
this.im = new SimpleStringProperty(im);
this.nom = new SimpleStringProperty(nom);
this.prenoms = new SimpleStringProperty(prenoms);
this.datenais = new SimpleStringProperty(datenais);
this.diplome = new SimpleStringProperty(diplome);
this.contact = new SimpleStringProperty(contact);
this.statut = new SimpleBooleanProperty(statut);
this.situation = new SimpleStringProperty(situation);
this.nomConjoint = new SimpleStringProperty(nomConjoint);
this.prenomConjoint = new SimpleStringProperty(prenomConjoint);
this.montant = new SimpleIntegerProperty(montant);
}
- Add the getter and property accessor methods below the constructor. Each field needs both a value getter and a property accessor:
public String getIm() { return im.get(); }
public SimpleStringProperty imProperty() { return im; }
public String getNom() { return nom.get(); }
public SimpleStringProperty nomProperty() { return nom; }
public String getPrenoms() { return prenoms.get(); }
public SimpleStringProperty prenomsProperty() { return prenoms; }
public String getDatenais() { return datenais.get(); }
public SimpleStringProperty datenaisProperty() { return datenais; }
public String getDiplome() { return diplome.get(); }
public SimpleStringProperty diplomeProperty() { return diplome; }
public String getContact() { return contact.get(); }
public SimpleStringProperty contactProperty() { return contact; }
public boolean getStatut() { return statut.get(); }
public SimpleBooleanProperty statutProperty() { return statut; }
public String getSituation() { return situation.get(); }
public SimpleStringProperty situationProperty() { return situation; }
public String getNomConjoint() { return nomConjoint.get(); }
public SimpleStringProperty nomConjointProperty() { return nomConjoint; }
public String getPrenomConjoint() { return prenomConjoint.get(); }
public SimpleStringProperty prenomConjointProperty() { return prenomConjoint; }
public int getMontant() { return montant.get(); }
public SimpleIntegerProperty montantProperty() { return montant; }
What do the property accessors do?
The PropertyValueFactory in the controller looks for a method named <fieldName>Property() on the model. For example, when you configure a column with new PropertyValueFactory<>("nom"), it calls nomProperty() to bind the cell to that property.
- Save Personne.java.
✔️ Awesome, I've got everything!
Great. Make sure your file is saved and the closing } brace is in place for the class.
ⓧ I'd like to double check the full code
Scroll up and confirm your Personne.java contains all three chunks in this order:
- Package, imports, class declaration, and 11 field declarations.
- Constructor accepting all 11 parameters and initializing each property.
- 11 pairs of getter + property accessor methods.
- A closing } brace for the class.
Create the MainController
The Controller is the Java class that receives the injected UI elements and tells each column which property to display.
- In NetBeans, right-click the com.pension source package and create a new package called controller.
- Inside com.pension.controller, create a new Java class file named MainController.java.
- Add the class structure with all the @FXML field declarations by pasting this code:
package com.pension.controller;
import com.pension.model.Personne;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
public class MainController {
// TableView and column references matching fx:ids from FXML
@FXML private TableView<Personne> tablePersonne;
@FXML private TableColumn<Personne, String> colIm;
@FXML private TableColumn<Personne, String> colNom;
@FXML private TableColumn<Personne, String> colPrenoms;
@FXML private TableColumn<Personne, String> colDatenais;
@FXML private TableColumn<Personne, String> colDiplome;
@FXML private TableColumn<Personne, Integer> colMontant;
@FXML private TableColumn<Personne, String> colContact;
@FXML private TableColumn<Personne, Boolean> colStatut;
@FXML private TableColumn<Personne, String> colSituation;
// Search and filter controls
@FXML private TextField searchField;
@FXML private ComboBox<String> filterStatut;
@FXML private Label labelEffectif;
}
What does @FXML do?
The @FXML annotation tells the FXMLLoader to inject the matching UI element (identified by fx:id) into this field when the FXML loads. The field name must match the fx:id you set in Scene Builder exactly.
- Add the initialize() method inside the class, below the field declarations:
@FXML
public void initialize() {
// Configure each column to display the matching Personne property
colIm.setCellValueFactory(new PropertyValueFactory<>("im"));
colNom.setCellValueFactory(new PropertyValueFactory<>("nom"));
colPrenoms.setCellValueFactory(new PropertyValueFactory<>("prenoms"));
colDatenais.setCellValueFactory(new PropertyValueFactory<>("datenais"));
colDiplome.setCellValueFactory(new PropertyValueFactory<>("diplome"));
colMontant.setCellValueFactory(new PropertyValueFactory<>("montant"));
colContact.setCellValueFactory(new PropertyValueFactory<>("contact"));
colStatut.setCellValueFactory(new PropertyValueFactory<>("statut"));
colSituation.setCellValueFactory(new PropertyValueFactory<>("situation"));
// Populate the status filter ComboBox
filterStatut.getItems().addAll("Tous", "Vivant", "Decede");
filterStatut.setValue("Tous");
}
What does this code do?
The initialize() method runs automatically after the FXML loads. setCellValueFactory tells each column which property to read from a Personne object.
The string passed to PropertyValueFactory (e.g. "nom") must match the property accessor name in your model (nomProperty()). The ComboBox is pre-populated with the three filter options.
- Save MainController.java.
✔️ Awesome, I've got everything!
Great. Double check that your file is saved and the closing } brace closes the class.
ⓧ I'd like to double check the full code
Scroll up and confirm your MainController.java contains both chunks in this order:
- Package, imports, class declaration, and all 13 @FXML field declarations (9 columns + tablePersonne + searchField + filterStatut + labelEffectif).
- The initialize() method with 9 setCellValueFactory calls and the ComboBox population.
- A closing } brace for the class.
Run the application and verify the layout
Your App.java already loads main-view.fxml (this was configured in Step 1). With the FXML, model, and controller all in place, the app should now display your full layout.
Before you run, think about what you expect to see. Will the TableView show data, or will it be empty? Why?
- In NetBeans, right-click your project and select Run, or run the following command in the terminal:
mvn clean javafx:run
The main window appears with the TableView (empty, because no data is loaded yet), the search TextField at the top, the ComboBox showing "Tous", the effectif label, and the three buttons at the bottom.
Seeing an error about the controller or FXML?
Check that your fx:id values in Scene Builder match the field names in MainController.java exactly. A single typo (e.g. colNoms vs colNom) causes a runtime injection failure.
Also confirm that the Controller class field in Scene Builder says com.pension.controller.MainController (the full package path). If it is blank or wrong, the loader cannot find the class.
Help me fix my FXML controller binding error
Your interface is designed and wired up. The TableView is empty because there is no database query loading data yet. In the next step, you will build the CRUD operations that populate this table from PostgreSQL.
Implement CRUD Operations
Right now your application displays an empty TableView. The layout exists, but it cannot read from or write to the database.
In this step, you will build the data access layer that talks to PostgreSQL, design a form dialog in Scene Builder, and wire everything together so users can create, read, update, and delete pensionnaires.
In this step, get ready to:
- Create the DAO classes that execute SQL queries against your database.
- Design a form dialog for adding and editing pensionnaires.
- Wire the Ajouter, Modifier, and Supprimer buttons to perform full CRUD operations.
Create PersonneDAO with the read operation
The DAO (Data Access Object) pattern separates database logic from your controllers. Each SQL operation lives in its own method.
- In NetBeans, right-click the com.pension package under Source Packages and create a new Java package named com.pension.dao.
- Create a new Java class named PersonneDAO.java inside com.pension.dao.
- Replace the file contents with the following code:
package com.pension.dao;
import com.pension.model.Personne;
import com.pension.util.DatabaseConnection;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.sql.*;
import java.util.LinkedHashMap;
import java.util.Map;
public class PersonneDAO {
// Retrieve all pensionnaires joined with tarif for montant
public ObservableList<Personne> getAll() {
ObservableList<Personne> list = FXCollections.observableArrayList();
String sql = "SELECT p.*, t.montant FROM personne p LEFT JOIN tarif t ON p.diplome = t.diplome";
try (Connection conn = DatabaseConnection.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
list.add(mapResultSet(rs));
}
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
What does this code do?
The getAll() method performs a LEFT JOIN between the personne and tarif tables. This pulls the pension montant for each person based on their diploma.
It returns an ObservableList, which is a special JavaFX collection that automatically notifies the TableView when data changes.
- Below the getAll() method, add the mapResultSet() helper and close the class:
// Convert a database row into a Personne object
private Personne mapResultSet(ResultSet rs) throws SQLException {
return new Personne(
rs.getString("im"),
rs.getString("nom"),
rs.getString("prenoms"),
rs.getString("datenais"),
rs.getString("diplome"),
rs.getString("contact"),
rs.getBoolean("statut"),
rs.getString("situation"),
rs.getString("nom_conjoint"),
rs.getString("prenom_conjoint"),
rs.getInt("montant")
);
}
}
What does mapResultSet do?
This private helper converts one row of a ResultSet into a Personne object. Every query method in this DAO reuses it, keeping the code DRY.
- Save PersonneDAO.java.
- Open MainController.java and add this field at the top of the class, below the @FXML declarations:
private final PersonneDAO personneDAO = new PersonneDAO();
- At the end of the initialize() method, add this line to load data on startup:
// Load data from the database
tablePersonne.setItems(personneDAO.getAll());
- Save MainController.java and add the import for com.pension.dao.PersonneDAO at the top of the file.
Before you run the app, do you expect the TableView to show data or remain empty? Think about whether you inserted any rows into the personne table.
- Run the app with mvn clean javafx:run (right-click the project, then Run).
The TableView stays empty because the personne table has no rows yet. The app runs without errors, which confirms your DAO connects to the database successfully.
Seeing a connection error?
- Confirm PostgreSQL is running. Open pgAdmin and check that pension_db appears under your server.
- Verify the password in DatabaseConnection.java matches your PostgreSQL password.
- Check that port 5432 is not blocked or used by another service.
Help me debug my JDBC connection error in PersonneDAO
Add the write operations and search methods
Now that read works, you need insert, update, delete, and search methods. These all follow the same PreparedStatement pattern you saw above.
- In PersonneDAO.java, add the insert() method below getAll():
// Insert a new pensionnaire into the database
public void insert(Personne p) {
String sql = "INSERT INTO personne (im, nom, prenoms, datenais, diplome, contact, statut, situation, nom_conjoint, prenom_conjoint) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, p.getIm());
pstmt.setString(2, p.getNom());
pstmt.setString(3, p.getPrenoms());
pstmt.setDate(4, Date.valueOf(p.getDatenais()));
pstmt.setString(5, p.getDiplome());
pstmt.setString(6, p.getContact());
pstmt.setBoolean(7, p.getStatut());
pstmt.setString(8, p.getSituation());
pstmt.setString(9, p.getNomConjoint());
pstmt.setString(10, p.getPrenomConjoint());
pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
Why PreparedStatement instead of Statement?
PreparedStatement uses ? placeholders instead of string concatenation. This prevents SQL injection attacks and handles special characters in names automatically.
- Below insert(), add the update() and delete() methods:
// Update an existing pensionnaire by IM
public void update(Personne p) {
String sql = "UPDATE personne SET nom=?, prenoms=?, datenais=?, diplome=?, contact=?, statut=?, situation=?, nom_conjoint=?, prenom_conjoint=? WHERE im=?";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, p.getNom());
pstmt.setString(2, p.getPrenoms());
pstmt.setDate(3, Date.valueOf(p.getDatenais()));
pstmt.setString(4, p.getDiplome());
pstmt.setString(5, p.getContact());
pstmt.setBoolean(6, p.getStatut());
pstmt.setString(7, p.getSituation());
pstmt.setString(8, p.getNomConjoint());
pstmt.setString(9, p.getPrenomConjoint());
pstmt.setString(10, p.getIm());
pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
// Delete a pensionnaire by their IM identifier
public void delete(String im) {
String sql = "DELETE FROM personne WHERE im=?";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, im);
pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
- Save PersonneDAO.java.
- Run the app again to confirm it compiles cleanly with no errors.
The app still shows an empty table. The new methods exist but nothing calls them yet. You will wire them to the buttons in substep 3.
- Now add the search() method below delete(). This combines keyword and status filtering in one query:
// Search by keyword and/or statut filter
public ObservableList<Personne> search(String keyword, String statut) {
ObservableList<Personne> list = FXCollections.observableArrayList();
StringBuilder sql = new StringBuilder("SELECT p.*, t.montant FROM personne p LEFT JOIN tarif t ON p.diplome = t.diplome WHERE 1=1");
if (keyword != null && !keyword.isEmpty()) {
sql.append(" AND (p.im LIKE ? OR p.nom LIKE ?)");
}
if (statut != null && !statut.equals("Tous")) {
sql.append(" AND p.statut = ?");
}
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql.toString())) {
int paramIndex = 1;
if (keyword != null && !keyword.isEmpty()) {
pstmt.setString(paramIndex++, "%" + keyword + "%");
pstmt.setString(paramIndex++, "%" + keyword + "%");
}
if (statut != null && !statut.equals("Tous")) {
pstmt.setBoolean(paramIndex, statut.equals("Vivant"));
}
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
list.add(mapResultSet(rs));
}
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
- Finally, add the getCountByDiplome() method below search() (just above mapResultSet):
// Count pensionnaires grouped by diploma for statistics
public Map<String, Integer> getCountByDiplome() {
Map<String, Integer> map = new LinkedHashMap<>();
String sql = "SELECT diplome, COUNT(*) as total FROM personne GROUP BY diplome";
try (Connection conn = DatabaseConnection.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
map.put(rs.getString("diplome"), rs.getInt("total"));
}
} catch (SQLException e) {
e.printStackTrace();
}
return map;
}
- Save PersonneDAO.java.
What are search and getCountByDiplome for?
The search() method powers the filter bar with SQL LIKE queries. The getCountByDiplome() method groups pensionnaires by diploma for statistics. You will wire both into the UI in later steps.
- Now create TarifDAO.java in the same com.pension.dao package. This DAO reads diploma options and their amounts from the TARIF table:
package com.pension.dao;
import com.pension.util.DatabaseConnection;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.sql.*;
public class TarifDAO {
// Get all distinct diploma names for the form ComboBox
public ObservableList<String> getAllDiplomes() {
ObservableList<String> list = FXCollections.observableArrayList();
String sql = "SELECT DISTINCT diplome FROM tarif";
try (Connection conn = DatabaseConnection.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
list.add(rs.getString("diplome"));
}
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
// Look up the montant for a given diploma
public int getMontantByDiplome(String diplome) {
String sql = "SELECT montant FROM tarif WHERE diplome = ?";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, diplome);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
return rs.getInt("montant");
}
} catch (SQLException e) {
e.printStackTrace();
}
return 0;
}
}
What does TarifDAO do?
The form dialog needs to show a dropdown of available diplomas. When the user selects one, getMontantByDiplome() automatically looks up the corresponding pension amount from the TARIF table.
- Save TarifDAO.java and run the app to confirm everything still compiles.
Seeing a compile error?
- Check that both DAO files are inside the com.pension.dao package.
- Make sure the mapResultSet() method in PersonneDAO sits inside the class braces (before the final }).
Help me fix a compile error in my DAO classes
✔️ Awesome, I've got everything!
Great. Make sure both PersonneDAO.java and TarifDAO.java are saved in com.pension.dao.
ⓧ I'd like to double check the full code
Scroll up and confirm your PersonneDAO.java contains, in this order:
- Package declaration and imports (including LinkedHashMap and Map).
- The getAll() method with the LEFT JOIN query.
- The insert() method with 10 PreparedStatement parameters.
- The update() and delete() methods.
- The search() method with StringBuilder query building.
- The getCountByDiplome() method returning a Map.
- The private mapResultSet() helper at the bottom.
For TarifDAO.java, confirm it has getAllDiplomes() and getMontantByDiplome().
Design the form dialog in Scene Builder
Users need a form to enter pensionnaire details. You will build this as a separate FXML file that opens as a modal dialog window.
- Open Scene Builder.
- Create a new FXML document.
- From the Containers section of the Library panel, drag a GridPane onto the canvas as the root element.
- Set the GridPane to have 2 columns and 10 rows using the Layout section in the Inspector panel.
- In column 0, add Label elements for: IM, Nom, Prenoms, Date Naissance, Diplome, Montant, Contact, Situation, Nom Conjoint, Prenom Conjoint.
- In column 1, add the input fields: TextField for IM (fx:id fieldIm), TextField for Nom (fieldNom), TextField for Prenoms (fieldPrenoms), DatePicker for Date Naissance (fieldDate), ComboBox for Diplome (comboDiplome), Label for Montant (labelMontant), TextField for Contact (fieldContact), ComboBox for Situation (comboSituation), TextField for Nom Conjoint (fieldNomConjoint), TextField for Prenom Conjoint (fieldPrenomConjoint).
- Below the GridPane, add an HBox with two Buttons: Enregistrer (fx:id btnSave, onAction #onSave) and Annuler (onAction #onCancel).
- In the Controller section at the bottom left of Scene Builder, set the Controller class to com.pension.controller.FormController.
- Save the file as form-personne.fxml in src/main/resources/com/pension/view/.
Why is Montant a Label and not a TextField?
The pension amount is determined by the diploma. Users should not type it manually. When they select a diploma from the ComboBox, the label updates automatically with the correct amount from the TARIF table.
- Now create FormController.java in com.pension.controller. This controller handles the form logic:
package com.pension.controller;
import com.pension.dao.PersonneDAO;
import com.pension.dao.TarifDAO;
import com.pension.model.Personne;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.stage.Stage;
import java.time.LocalDate;
public class FormController {
@FXML private TextField fieldIm;
@FXML private TextField fieldNom;
@FXML private TextField fieldPrenoms;
@FXML private DatePicker fieldDate;
@FXML private ComboBox<String> comboDiplome;
@FXML private Label labelMontant;
@FXML private TextField fieldContact;
@FXML private ComboBox<String> comboSituation;
@FXML private TextField fieldNomConjoint;
@FXML private TextField fieldPrenomConjoint;
- Below the field declarations, add the DAO instances and the initialize() method:
private final TarifDAO tarifDAO = new TarifDAO();
private final PersonneDAO personneDAO = new PersonneDAO();
private Personne currentPersonne;
@FXML
public void initialize() {
// Populate diploma ComboBox from TARIF table
comboDiplome.setItems(tarifDAO.getAllDiplomes());
// Auto-update montant when diploma selection changes
comboDiplome.setOnAction(e -> {
int montant = tarifDAO.getMontantByDiplome(comboDiplome.getValue());
labelMontant.setText(String.valueOf(montant));
});
// Populate situation ComboBox
comboSituation.getItems().addAll("Marie(e)", "Divorce(e)", "Veuf(ve)");
}
- Add the setPersonne() method that pre-fills the form when editing:
// Pre-fill form fields when editing an existing pensionnaire
public void setPersonne(Personne p) {
if (p != null) {
currentPersonne = p;
fieldIm.setText(p.getIm());
fieldIm.setDisable(true);
fieldNom.setText(p.getNom());
fieldPrenoms.setText(p.getPrenoms());
fieldDate.setValue(LocalDate.parse(p.getDatenais()));
comboDiplome.setValue(p.getDiplome());
labelMontant.setText(String.valueOf(p.getMontant()));
fieldContact.setText(p.getContact());
comboSituation.setValue(p.getSituation());
fieldNomConjoint.setText(p.getNomConjoint());
fieldPrenomConjoint.setText(p.getPrenomConjoint());
}
}
- Finally, add the onSave() and onCancel() handlers and close the class:
@FXML
private void onSave() {
// Build Personne from form fields
Personne p = new Personne(
fieldIm.getText(), fieldNom.getText(), fieldPrenoms.getText(),
fieldDate.getValue().toString(), comboDiplome.getValue(),
fieldContact.getText(), true, comboSituation.getValue(),
fieldNomConjoint.getText(), fieldPrenomConjoint.getText(),
Integer.parseInt(labelMontant.getText())
);
if (currentPersonne == null) {
personneDAO.insert(p);
} else {
personneDAO.update(p);
}
closeWindow();
}
@FXML
private void onCancel() {
closeWindow();
}
private void closeWindow() {
Stage stage = (Stage) fieldIm.getScene().getWindow();
stage.close();
}
}
What does this controller do?
When currentPersonne is null, the form is in "add" mode and calls insert(). When it holds an existing person (set by setPersonne()), the form is in "edit" mode and calls update().
The diploma ComboBox triggers an auto-lookup. Selecting "LICENCE" immediately shows 250000 in the montant label.
- Save FormController.java.
Wire the button handlers in MainController
The DAO and form exist. Now you connect the three buttons in the main view to open the form, delete records, and refresh the table after every change.
- Open MainController.java. Replace the temporary tablePersonne.setItems(personneDAO.getAll()) line in initialize() with a call to a new refreshTable() method. Add this private method to the class:
// Reload the table using current search and filter values
private void refreshTable() {
String keyword = searchField.getText();
String statut = filterStatut.getValue();
ObservableList<Personne> list = personneDAO.search(keyword, statut);
tablePersonne.setItems(list);
labelEffectif.setText("Effectif: " + list.size());
}
- Add the openForm() helper method that loads the form FXML as a modal dialog:
// Open the form dialog for adding or editing
private void openForm(Personne personne) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/pension/view/form-personne.fxml"));
Parent root = loader.load();
FormController formController = loader.getController();
formController.setPersonne(personne);
Stage stage = new Stage();
stage.setTitle(personne == null ? "Ajouter Pensionnaire" : "Modifier Pensionnaire");
stage.setScene(new Scene(root));
stage.initModality(Modality.APPLICATION_MODAL);
stage.showAndWait();
refreshTable();
} catch (Exception e) {
e.printStackTrace();
}
}
- Add the three button handler methods annotated with @FXML:
@FXML
private void onAjouter() {
openForm(null);
}
@FXML
private void onModifier() {
Personne selected = tablePersonne.getSelectionModel().getSelectedItem();
if (selected != null) {
openForm(selected);
}
}
@FXML
private void onSupprimer() {
Personne selected = tablePersonne.getSelectionModel().getSelectedItem();
if (selected != null) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "Supprimer " + selected.getNom() + " ?");
java.util.Optional<ButtonType> result = alert.showAndWait();
if (result.isPresent() && result.get() == ButtonType.OK) {
personneDAO.delete(selected.getIm());
refreshTable();
}
}
}
What do these handlers do?
- onAjouter() passes null to openForm(), which tells FormController to use insert mode.
- onModifier() grabs the selected row and passes it to the form, enabling edit mode.
- onSupprimer() shows a confirmation Alert before calling delete(). After deletion, it refreshes the table.
- Make sure your imports at the top of MainController include javafx.stage.Modality, javafx.stage.Stage, javafx.scene.Parent, and javafx.scene.Scene.
- Also import com.pension.dao.PersonneDAO and com.pension.controller.FormController.
- In your initialize() method, replace the old tablePersonne.setItems(...) line with just refreshTable();.
- Save MainController.java.
Before you run, predict: when you click Ajouter and fill in the form with diploma "LICENCE", what amount should appear in the Montant label?
- Run the app with mvn clean javafx:run.
- Click Ajouter. The form dialog opens.
- Enter IM001 for IM, Rakoto for Nom, Jean for Prenoms.
- Pick any date in the DatePicker.
- Select LICENCE in the Diplome ComboBox. The Montant label updates to 250000.
- Select Marie(e) for Situation.
- Click Enregistrer.
The new pensionnaire appears in the TableView with all fields populated, including the montant of 250000.
- Select the row you just added.
- Click Supprimer. A confirmation dialog appears.
- Click OK.
The row disappears from the TableView. Your full CRUD cycle is working.
Form dialog not opening?
- Verify that form-personne.fxml is saved in src/main/resources/com/pension/view/. The path must match the getResource() call exactly.
- Check that the Controller class in Scene Builder is set to com.pension.controller.FormController.
- If you see a NullPointerException, confirm all fx:id values in the FXML match the @FXML field names in FormController.
Help me debug my form dialog that won't open in JavaFX
Your application now has full CRUD capability. Users can add pensionnaires, edit their details, and remove them. Next up, you will implement the survivor logic that automatically transfers 40% of the pension to the spouse when a pensionnaire is marked as deceased.
Implement the Survivor Logic (40% Rule)
Your CRUD operations work. You can add, edit, and delete pensionnaires from the database.
But right now, marking someone as deceased does nothing special. In a real pension system, when a married pensionnaire dies, 40% of their pension transfers automatically to their surviving spouse. This is the core business rule your application needs to enforce.
In this step, get ready to:
- Create the ConjointDAO to manage spouse pension records.
- Add the 40% transfer logic to the form save handler.
- Add a Conjoints tab to display spouse pension transfers.
Create ConjointDAO.java
The CONJOINT table stores spouse pension records. You need a DAO class to insert new transfers and retrieve all existing ones.
- In NetBeans, right-click the com.pension.dao package and create a new Java class named ConjointDAO.
- Add the package declaration, imports, and the insert() method by pasting this code:
package com.pension.dao;
import com.pension.util.DatabaseConnection;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.sql.*;
public class ConjointDAO {
// Insert a new spouse pension record
public void insert(String numPension, String nomConjoint, String prenomConjoint, int montant) {
String sql = "INSERT INTO conjoint (num_pension, nom_conjoint, prenom_conjoint, montant) VALUES (?, ?, ?, ?)";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, numPension);
pstmt.setString(2, nomConjoint);
pstmt.setString(3, prenomConjoint);
pstmt.setInt(4, montant);
pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
What does this code do?
The insert() method takes four parameters: a generated pension number, the spouse's name, and the transferred amount. It uses a PreparedStatement to safely insert the record into the CONJOINT table.
- Now add the getAll() method and close the class by pasting this code directly below the insert() method:
// Retrieve all spouse pension records
public ObservableList<String[]> getAll() {
ObservableList<String[]> list = FXCollections.observableArrayList();
String sql = "SELECT * FROM conjoint";
try (Connection conn = DatabaseConnection.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
list.add(new String[]{
rs.getString("num_pension"),
rs.getString("nom_conjoint"),
rs.getString("prenom_conjoint"),
String.valueOf(rs.getInt("montant"))
});
}
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
}
Why return String[] instead of a model class?
The CONJOINT table is a simple lookup with only four fields. Using a String[] keeps it lightweight. Each array element maps to one column: pension number, spouse last name, spouse first name, and amount.
- Save ConjointDAO.java.
- Right-click the project in NetBeans and select Build to confirm there are no compilation errors.
Seeing a compilation error?
- Check that the package declaration at the top says package com.pension.dao; and that the file is inside the correct folder.
- Ensure the closing brace } of the class is present after the getAll() method.
- help me fix my ConjointDAO compilation error
✔️ Awesome, I've got everything!
Great. Make sure you have saved ConjointDAO.java and the build succeeds with no errors.
ⓧ I'd like to double check the full code
Here is the complete ConjointDAO.java file:
package com.pension.dao;
import com.pension.util.DatabaseConnection;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.sql.*;
public class ConjointDAO {
public void insert(String numPension, String nomConjoint, String prenomConjoint, int montant) {
String sql = "INSERT INTO conjoint (num_pension, nom_conjoint, prenom_conjoint, montant) VALUES (?, ?, ?, ?)";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, numPension);
pstmt.setString(2, nomConjoint);
pstmt.setString(3, prenomConjoint);
pstmt.setInt(4, montant);
pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
public ObservableList<String[]> getAll() {
ObservableList<String[]> list = FXCollections.observableArrayList();
String sql = "SELECT * FROM conjoint";
try (Connection conn = DatabaseConnection.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
list.add(new String[]{
rs.getString("num_pension"),
rs.getString("nom_conjoint"),
rs.getString("prenom_conjoint"),
String.valueOf(rs.getInt("montant"))
});
}
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
}
Add the 40% survivor transfer logic to FormController
When a user changes a pensionnaire's status from Vivant to Decede, your app needs to check three conditions: the person was alive before, they are married, and they have a spouse name on file. If all three are true, 40% of their pension transfers to the spouse automatically.
- Open FormController.java in NetBeans.
- Add import statements for ConjointDAO and TarifDAO at the top of the file if they are not already imported:
import com.pension.dao.ConjointDAO;
import com.pension.dao.TarifDAO;
import javafx.scene.control.Alert;
- Inside your save handler method (the method that fires when the user clicks Save), add the survivor logic after the personneDAO.update() call. Paste this code:
// Survivor logic: transfer 40% pension to spouse when pensionnaire dies
if (!personne.getStatut() && originalStatut) {
// Statut changed from Vivant (true) to Decede (false)
if ("Marie(e)".equals(personne.getSituation()) &&
personne.getNomConjoint() != null && !personne.getNomConjoint().isEmpty()) {
// Calculate 40% of the original pension amount
TarifDAO tarifDAO = new TarifDAO();
int montantOriginal = tarifDAO.getMontantByDiplome(personne.getDiplome());
int montantConjoint = (int)(montantOriginal * 0.4);
// Generate pension number and insert into CONJOINT table
String numPension = "CONJ-" + personne.getIm();
ConjointDAO conjointDAO = new ConjointDAO();
conjointDAO.insert(numPension, personne.getNomConjoint(),
personne.getPrenomConjoint(), montantConjoint);
// Inform the user of the automatic transfer
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Transfert Pension");
alert.setContentText("40% de la pension transfere au conjoint: "
+ montantConjoint + " Ar");
alert.showAndWait();
}
}
What does this code do?
- The condition !personne.getStatut() && originalStatut checks that the status just changed from Vivant (true) to Decede (false).
- It then verifies the person is married and has a spouse name before calculating 40% of the pension via TarifDAO.getMontantByDiplome().
- The pension number is generated as "CONJ-" + im to link back to the original pensionnaire.
What is originalStatut?
You need a boolean variable originalStatut that stores the pensionnaire's statut when the form first opens. Set it in your setPersonne() method: this.originalStatut = personne.getStatut(); This lets you detect a status change at save time.
- Add a field private boolean originalStatut; at the top of FormController.
- In your setPersonne() method, store the original statut value: this.originalStatut = personne != null && personne.getStatut();
- Save FormController.java.
Before you run the app, what do you think will happen when you mark a married pensionnaire as deceased?
- Run the application with mvn clean javafx:run.
- Click Ajouter and create a test pensionnaire with statut set to Vivant, situation set to Marie(e), and a spouse name filled in.
- Select that pensionnaire in the TableView and click Modifier.
- Change the statut to Decede and click Save.
You should see an Alert dialog appear confirming the 40% pension transfer to the spouse, showing the calculated amount.
No Alert appearing when you change statut to Decede?
- Verify that originalStatut is set in setPersonne() before the form opens. If it is not stored, the condition always evaluates to false.
- Check that the situation field value matches exactly: "Marie(e)". A different spelling like "Marie" or "Marié" will fail the comparison.
- Make sure the nom_conjoint field is not empty in the form.
- help me debug why the 40% survivor transfer is not triggering
Add a Conjoints tab to the main interface
The transfer works in the database, but the user cannot see the results without opening pgAdmin. Adding a second tab with the CONJOINT table data lets them verify transfers directly in the app.
- Open main-view.fxml in Scene Builder.
- Wrap the existing center content (the TableView and surrounding elements) inside a TabPane. You can find TabPane in the Containers section of the Library panel on the left.
- Name the first tab Pensionnaires and place the existing TableView content inside it.
- Add a second tab named Conjoints.
- Inside the Conjoints tab, drag in a TableView and set its fx:id to tableConjoint.
- Add four TableColumns to the new TableView with these fx:id values: colConjNumPension, colConjNom, colConjPrenom, colConjMontant.
- Set the column header text to: N° Pension, Nom Conjoint, Prenom Conjoint, Montant.
- Save the FXML file in Scene Builder.
What is a TabPane?
A TabPane is a JavaFX container that organizes content into tabs the user can switch between. Each tab holds its own layout, letting you show different views without navigating to a new window.
Now you need to wire the new TableView to the data in MainController.
- Switch back to MainController.java in NetBeans.
- Add the new FXML field declarations alongside the existing ones by pasting this code:
// Conjoints tab fields
@FXML private TableView<String[]> tableConjoint;
@FXML private TableColumn<String[], String> colConjNumPension;
@FXML private TableColumn<String[], String> colConjNom;
@FXML private TableColumn<String[], String> colConjPrenom;
@FXML private TableColumn<String[], String> colConjMontant;
// DAO for conjoint table access
private final ConjointDAO conjointDAO = new ConjointDAO();
What does this code do?
These @FXML annotations tell JavaFX to inject the TableView and TableColumn elements from the FXML file (matching the fx:id values you set in Scene Builder). The conjointDAO instance provides access to the CONJOINT table data.
- Add the import for ConjointDAO at the top of the file:
import com.pension.dao.ConjointDAO;
- Add the refreshConjointTable() method below the existing refreshTable() method by pasting this code:
private void refreshConjointTable() {
ObservableList<String[]> list = conjointDAO.getAll();
tableConjoint.setItems(list);
// Map each array index to a table column
colConjNumPension.setCellValueFactory(data ->
new javafx.beans.property.SimpleStringProperty(data.getValue()[0]));
colConjNom.setCellValueFactory(data ->
new javafx.beans.property.SimpleStringProperty(data.getValue()[1]));
colConjPrenom.setCellValueFactory(data ->
new javafx.beans.property.SimpleStringProperty(data.getValue()[2]));
colConjMontant.setCellValueFactory(data ->
new javafx.beans.property.SimpleStringProperty(data.getValue()[3]));
}
Why use SimpleStringProperty with array indices?
Since getAll() returns String[] arrays instead of a model class, you cannot use PropertyValueFactory. Instead, each column maps to an array index using a lambda that wraps the value in a SimpleStringProperty.
- In the initialize() method, find the line that calls refreshTable(); and add a call to refreshConjointTable(); directly below it.
- In the openForm() method, find the line refreshTable(); that runs after stage.showAndWait(); and add refreshConjointTable(); directly below it.
- Save MainController.java.
Seeing a NullPointerException for tableConjoint?
- Verify the fx:id in Scene Builder matches exactly: tableConjoint. Capitalization matters.
- Make sure you saved the FXML file in Scene Builder before running the app in NetBeans.
- Check that the Controller class is still set to com.pension.controller.MainController in Scene Builder's Document section.
- help me fix a NullPointerException on tableConjoint in my MainController
Verify the full survivor workflow
Before you run the app, predict: what will happen in the Conjoints tab after you mark a married pensionnaire as deceased?
- Run the application with mvn clean javafx:run.
- In the Pensionnaires tab, add a new pensionnaire with statut Vivant, situation Marie(e), a diploma (e.g. LICENCE), and a spouse name.
- Select that pensionnaire and click Modifier.
- Change the statut to Decede and click Save.
- Confirm the Alert appears showing the 40% amount (for LICENCE with montant 250000, you should see 100000 Ar).
- Click the Conjoints tab. The spouse now appears with the pension number, name, and the 40% amount.
- Open pgAdmin and run a SELECT query on the CONJOINT table to confirm the row was inserted in the database.
You should see a row with the pension number (e.g. CONJ-IM001), the spouse name, and the calculated 40% amount.
The 40% survivor rule is the heart of this pension system. Next, you will add search and status filtering so users can find pensionnaires quickly in a large dataset.
Add Search and Status Filter
Your pension app can now add, edit, delete, and even handle the automatic survivor transfer. That covers the full lifecycle of a pensionnaire record.
But imagine having dozens of records in the database. Scrolling through the entire TableView to find one person is impractical for daily use. You need a way to search by name or IM number and filter by status so administrators can locate records instantly.
In this step, get ready to:
- Understand how the SQL LIKE query filters pensionnaires by partial keyword.
- Wire the search field and status ComboBox to filter the TableView in real time.
- Verify that search and filter work together with the effectif count updating.
Walk through the search query in PersonneDAO
Your PersonneDAO.java already contains a search(String keyword, String statut) method that builds a dynamic SQL query. Before wiring it to the UI, let's understand how it works.
- In NetBeans, open PersonneDAO.java and scroll to the search method.
- Find this code block:
public ObservableList<Personne> search(String keyword, String statut) {
ObservableList<Personne> list = FXCollections.observableArrayList();
StringBuilder sql = new StringBuilder("SELECT p.*, t.montant FROM personne p LEFT JOIN tarif t ON p.diplome = t.diplome WHERE 1=1");
if (keyword != null && !keyword.isEmpty()) {
sql.append(" AND (p.im LIKE ? OR p.nom LIKE ?)");
}
if (statut != null && !statut.equals("Tous")) {
sql.append(" AND p.statut = ?");
}
What does this query do?
The method builds its SQL query piece by piece using a StringBuilder. It starts with WHERE 1=1 as a base condition that always passes. This trick makes it easy to append AND clauses conditionally without worrying about whether a preceding WHERE already exists.
The LIKE operator with "%" + keyword + "%" performs a partial match. Typing "Rak" would match "Rakoto", "Rakotondrabe", or any name containing those letters.
Why use PreparedStatement instead of string concatenation?
PreparedStatement prevents SQL injection attacks. If you concatenated user input directly into the query string, a malicious user could type '; DROP TABLE personne; -- and destroy your data. The ? placeholders ensure user input is always treated as data, never as SQL commands.
Wire the search and filter listeners in MainController
The UI elements exist in your FXML, but they are not connected to the search logic yet. You need to populate the ComboBox with filter options and add listeners that call refreshTable() whenever the user types or changes the filter.
- In MainController.java, find the initialize() method.
- After the setCellValueFactory lines for your TableColumns, add the following code to populate the ComboBox and wire the listeners:
// Populate the status filter ComboBox with options
filterStatut.getItems().addAll("Tous", "Vivant", "Decede");
filterStatut.setValue("Tous");
// Refresh the table whenever the filter selection changes
filterStatut.setOnAction(e -> refreshTable());
// Refresh the table on every keystroke in the search field
searchField.textProperty().addListener((obs, oldVal, newVal) -> refreshTable());
// Load initial data
refreshTable();
refreshConjointTable();
What does this code do?
- filterStatut.getItems().addAll("Tous", "Vivant", "Decede") fills the ComboBox with three options: all records, living only, or deceased only.
- filterStatut.setValue("Tous") sets the default selection so all pensionnaires appear on launch.
- setOnAction fires each time the user picks a new item from the ComboBox dropdown.
- textProperty().addListener() is a ChangeListener that fires on every keystroke, giving real-time search results.
Now the listeners call refreshTable(), but that method needs to use the search query instead of loading all records blindly.
- In MainController.java, find the refreshTable() method and replace its body with the following code:
private void refreshTable() {
// Read current search keyword and filter selection
String keyword = searchField.getText();
String statut = filterStatut.getValue();
// Query the database with both filters applied
ObservableList<Personne> list = personneDAO.search(keyword, statut);
// Update the TableView and effectif count
tablePersonne.setItems(list);
labelEffectif.setText("Effectif: " + list.size());
}
What does this code do?
Every time the user types a character or changes the filter, this method reads BOTH the current keyword and the current statut selection. It passes both to personneDAO.search(), which builds the appropriate SQL query with one or both WHERE clauses. The effectif label always reflects how many records match the current combination of search and filter.
- Save MainController.java.
Seeing a compilation error?
- Make sure you have import javafx.collections.ObservableList; at the top of your file.
- Check that searchField is declared as @FXML private TextField searchField; and matches the fx:id in your FXML.
- Verify that filterStatut is declared as @FXML private ComboBox<String> filterStatut; with the generic type specified.
Help me debug a compilation error in my MainController
Test search and filter together
Before you run the app, think about what should happen. If you type a partial name and also select a status filter, do you expect the results to apply BOTH conditions or just one?
- Run the application by executing:
mvn clean javafx:run
- Type a partial name into the search field (for example, the first few letters of a pensionnaire's name).
- Observe that the TableView updates immediately, showing only matching records.
- Clear the search field.
- Select Vivant in the status ComboBox.
- Observe that the TableView shows only living pensionnaires, and the labelEffectif updates to show the correct count.
- Now type a partial name while Vivant is still selected. The results narrow further, applying both conditions with AND.
The label should read something like "Effectif: 2" (the count matching your combined filters). Selecting Tous brings back all records.
TableView not updating when you type?
- Confirm that searchField.textProperty().addListener(...) is inside your initialize() method, not outside it.
- Check that the fx:id in your main-view.fxml for the search TextField matches exactly searchField (case-sensitive).
- If the effectif stays at 0 when you have data, verify that personneDAO.search() returns results by checking your database connection in DatabaseConnection.java.
Help me debug why my search filter is not working
✔️ Awesome, I've got everything!
Great. Double check you've saved MainController.java and that both search and filter update the table correctly.
ⓧ I'd like to double check the full code
Scroll up and confirm your MainController.java contains the following in order:
- All your existing @FXML field declarations including searchField, filterStatut, and labelEffectif.
- In initialize(): the setCellValueFactory lines, then the ComboBox population, then the two listeners, then refreshTable() and refreshConjointTable().
- The refreshTable() method reads both searchField.getText() and filterStatut.getValue(), calls personneDAO.search(), updates tablePersonne, and sets labelEffectif text.
- The existing onAjouter(), onModifier(), onSupprimer(), and openForm() methods remain unchanged.
Your pension management app is now fully functional for daily use. Administrators can search through records instantly and filter by status to get a quick headcount. Next up, tackle the Secret Mission to add a visual histogram showing how pensionnaires are distributed across diploma categories.
Secret mission
Add a Histogram (BarChart)
Your pension application handles data well, but administrators still need to open pgAdmin to understand the overall distribution. What if the app itself could show a visual breakdown of pensionnaires by diploma category? Add a BarChart that answers this question at a glance.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with a PostgreSQL database on your machine, so there are no ongoing cloud costs.
Resources you used:
- The gestion-pension Maven project folder on your Desktop.
- The pension_db PostgreSQL database with tables (tarif, personne, conjoint, payer).
Keep everything running
No action needed. Choose this if you are still actively building or want to continue experimenting with the application.
- Your gestion-pension project folder stays on your Desktop, ready to open in NetBeans.
- Your pension_db database stays available in pgAdmin for queries and testing.
- PostgreSQL continues running as a background service on your machine.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your files and database stay intact for when you return.
- Close NetBeans IDE if it is still open.
- Close Scene Builder if it is still open.
- Close pgAdmin in your browser.
- Your project folder and database remain on disk and are ready to use when you come back.
Delete - I don't want to use this again
Remove all project resources and start fresh. This permanently deletes your project code and database data.
- Delete the project folder. On macOS/Linux, run the following command in your terminal:
rm -rf ~/Desktop/gestion-pension
On Windows, run this in PowerShell:
Remove-Item -Recurse -Force ~\Desktop\gestion-pension
- Drop the database in pgAdmin:
- Open pgAdmin and connect to your local PostgreSQL server.
- In the left sidebar, expand **Servers**, then expand your local server.
- Right-click on pension_db under **Databases**.
- Select **Delete/Drop** and confirm when prompted.
Well Done!
Well Done!
Nice work! You just built a fully functional pension management desktop application using JavaFX and Scene Builder, connected to a live PostgreSQL database.
You've learned how to:
- Built a data-driven JavaFX interface with Scene Builder, connecting FXML layouts to Java controllers for a complete desktop application.
- Connected JavaFX to PostgreSQL using JDBC with PreparedStatements for full CRUD operations, search with SQL LIKE queries, and status filtering.
- Implemented the 40% survivor business rule that automatically transfers a pension to the surviving spouse when a pensionnaire is marked as deceased.
- Secret Mission: Added a BarChart histogram showing the distribution of pensionnaires by diploma category using a GROUP BY SQL query.
Ready to quiz yourself?