Ruben Laguna’s blog

JTable Bound to a Database With Lazy Loading

I’ve been doing experiments with JTables bound to database tables in the past. But I was not satisfied with the result to be honest. The pagination helps but there is still noticeable pauses each time a database query has to be issued. So I started looking into other possibilities.

I come up with the idea of doing lazy loading and presenting some fake data until the data is really retrieved from the database, I did a small proof of concept and seems to work ok.

The solution goes like this. It uses Beans Binding to bind a JTable to a custom List like in the previous post. This custom list (ResultListDerby in the example below) returns @Bean@s (the Bean is called Customer in the example) that are initially empty. By empty I mean that all its properties return something like “<data not retrieved from the database yet>”. So the JTable will be initially full of “<data not retrieved yet>”.

The picture above shows the JTable showing the List @of @Customers. Each row represents a Customer instance.

Having empty/fake Customer beans allow a fast drawing to screen but it’s not very useful. Now, if at the same point as each Customer is created we create SwingWorker and schedule it to run in a Executor then we can get something more meaningful. So what we do in the SwingWorker? The SwingWorker, you guessed it, will retrieve the data from the database and update the bean (look at ResultListDerby.getItem() method). When the bean (Customer.java) is updated it will notify its listeners of the changes, automatically by means of bound properties , in this case JTableBinding will be notified and the JTable that will repaint the cells to reflect the changes in the Bean. (Check how bound properties are implemented with of PropertyChangeSupport in Customer.java below). The rows will be updated one by one, as soon as the Executor completes a SwingWorker task a row will be updated.

This solution is free from annoying GUI freezes. The beans are always created fast because there is no need to go to the database to create them and that keeps the GUI liveness high. By moving the database access to @SwingWorker@s we keep the EDT (event dispatch thread) lightweight. The solution also avoids having too many running @SwingWorker@s at the same time by using a ExecutorService instead of executing them directly via the SwingWorker.execute() method (Check ResultListDerby.getItem() to see how it’s done).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
<span class='line'>package jtablepagination;
</span><span class='line'>
</span><span class='line'>import java.lang.ref.WeakReference;
</span><span class='line'>import java.sql.Connection;
</span><span class='line'>import java.sql.DriverManager;
</span><span class='line'>import java.sql.ResultSet;
</span><span class='line'>import java.sql.SQLException;
</span><span class='line'>import java.util.AbstractList;
</span><span class='line'>import java.util.HashMap;
</span><span class='line'>import java.util.List;
</span><span class='line'>import java.util.Map;
</span><span class='line'>import java.util.concurrent.ExecutorService;
</span><span class='line'>import java.util.concurrent.Executors;
</span><span class='line'>import java.util.logging.Level;
</span><span class='line'>import java.util.logging.Logger;
</span><span class='line'>import javax.swing.SwingWorker;
</span><span class='line'>
</span><span class='line'>/**
</span><span class='line'> *
</span><span class='line'> * @author Ruben Laguna &lt;ruben.laguna at gmail.com>
</span><span class='line'> */
</span><span class='line'>public class MainWindow extends javax.swing.JFrame {
</span><span class='line'>
</span><span class='line'>    
</span><span class='line'>
</span><span class='line'>    /** Creates new form MainWindow */
</span><span class='line'>    public MainWindow() {
</span><span class='line'>        initComponents();
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    /** This method is called from within the constructor to
</span><span class='line'>     * initialize the form.
</span><span class='line'>     * WARNING: Do NOT modify this code. The content of this method is
</span><span class='line'>     * always regenerated by the Form Editor.
</span><span class='line'>     */
</span><span class='line'>    @SuppressWarnings("unchecked")
</span><span class='line'>    // &lt;editor-fold defaultstate="collapsed" desc="Generated Code">                          
</span><span class='line'>    private void initComponents() {
</span><span class='line'>        bindingGroup = new org.jdesktop.beansbinding.BindingGroup();
</span><span class='line'>
</span><span class='line'>        list1 = getList();
</span><span class='line'>        jScrollPane1 = new javax.swing.JScrollPane();
</span><span class='line'>        jTable1 = new javax.swing.JTable();
</span><span class='line'>
</span><span class='line'>        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
</span><span class='line'>
</span><span class='line'>        org.jdesktop.swingbinding.JTableBinding jTableBinding = org.jdesktop.swingbinding.SwingBindings.createJTableBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ_WRITE, list1, jTable1);
</span><span class='line'>        org.jdesktop.swingbinding.JTableBinding.ColumnBinding columnBinding = jTableBinding.addColumnBinding(org.jdesktop.beansbinding.ELProperty.create("${id}"));
</span><span class='line'>        columnBinding.setColumnName("id");
</span><span class='line'>        columnBinding.setColumnClass(Integer.class);
</span><span class='line'>        columnBinding = jTableBinding.addColumnBinding(org.jdesktop.beansbinding.ELProperty.create("${firstName}"));
</span><span class='line'>        columnBinding.setColumnName("first name");
</span><span class='line'>        columnBinding.setColumnClass(String.class);
</span><span class='line'>        columnBinding = jTableBinding.addColumnBinding(org.jdesktop.beansbinding.ELProperty.create("${lastName}"));
</span><span class='line'>        columnBinding.setColumnName("last name");
</span><span class='line'>        columnBinding.setColumnClass(String.class);
</span><span class='line'>        bindingGroup.addBinding(jTableBinding);
</span><span class='line'>        jTableBinding.bind();
</span><span class='line'>        jScrollPane1.setViewportView(jTable1);
</span><span class='line'>
</span><span class='line'>        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
</span><span class='line'>        getContentPane().setLayout(layout);
</span><span class='line'>        layout.setHorizontalGroup(
</span><span class='line'>            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
</span><span class='line'>            .addGroup(layout.createSequentialGroup()
</span><span class='line'>                .addContainerGap()
</span><span class='line'>                .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 464, Short.MAX_VALUE)
</span><span class='line'>                .addContainerGap())
</span><span class='line'>        );
</span><span class='line'>        layout.setVerticalGroup(
</span><span class='line'>            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
</span><span class='line'>            .addGroup(layout.createSequentialGroup()
</span><span class='line'>                .addContainerGap()
</span><span class='line'>                .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 344, Short.MAX_VALUE)
</span><span class='line'>                .addContainerGap())
</span><span class='line'>        );
</span><span class='line'>
</span><span class='line'>        bindingGroup.bind();
</span><span class='line'>
</span><span class='line'>        pack();
</span><span class='line'>    }// &lt;/editor-fold>                        
</span><span class='line'>
</span><span class='line'>
</span><span class='line'>
</span><span class='line'>    private List&lt;Customer> getList() {
</span><span class='line'>        List&lt;Customer> toReturn = new ResultListDerby();
</span><span class='line'>        return toReturn;
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    /**
</span><span class='line'>    * @param args the command line arguments
</span><span class='line'>    */
</span><span class='line'>    public static void main(String args[]) {
</span><span class='line'>        java.awt.EventQueue.invokeLater(new Runnable() {
</span><span class='line'>            public void run() {
</span><span class='line'>                new MainWindow().setVisible(true);
</span><span class='line'>            }
</span><span class='line'>        });
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    // Variables declaration - do not modify                     
</span><span class='line'>    private javax.swing.JScrollPane jScrollPane1;
</span><span class='line'>    private javax.swing.JTable jTable1;
</span><span class='line'>    private java.util.List&lt;Customer> list1;
</span><span class='line'>    private org.jdesktop.beansbinding.BindingGroup bindingGroup;
</span><span class='line'>    // End of variables declaration                   
</span><span class='line'>
</span><span class='line'>}
</span><span class='line'>
</span><span class='line'> class ResultListDerby extends AbstractList {
</span><span class='line'>
</span><span class='line'>    private Connection connection;
</span><span class='line'>    private final ExecutorService ex = Executors.newSingleThreadExecutor();
</span><span class='line'>    private int size = -1;
</span><span class='line'>    //maintain a cache with the Customer instances already created and alive
</span><span class='line'>    private Map&lt;Integer, WeakReference&lt;Customer>> cache = new HashMap&lt;Integer, WeakReference&lt;Customer>>();
</span><span class='line'>
</span><span class='line'>    ResultListDerby() {
</span><span class='line'>        try {
</span><span class='line'>            Class.forName("org.apache.derby.jdbc.ClientDriver");
</span><span class='line'>            this.connection = DriverManager.getConnection("jdbc:derby://localhost:1527/customer", "nbuser", "nbuser");
</span><span class='line'>        } catch (Exception ex) {
</span><span class='line'>            Logger.getLogger(ResultListDerby.class.getName()).log(Level.SEVERE, null, ex);
</span><span class='line'>            throw new RuntimeException(ex);
</span><span class='line'>        }
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public int size() {
</span><span class='line'>        if (this.size == -1) {
</span><span class='line'>            try {
</span><span class='line'>                final ResultSet resultset = connection.createStatement().executeQuery("SELECT COUNT(ID) FROM CUSTOMERS");
</span><span class='line'>                resultset.next();
</span><span class='line'>                final int toReturn = resultset.getInt(1);
</span><span class='line'>                this.size = toReturn;
</span><span class='line'>            } catch (SQLException ex) {
</span><span class='line'>                Logger.getLogger(ResultListDerby.class.getName()).log(Level.SEVERE, null, ex);
</span><span class='line'>                throw new RuntimeException(ex);
</span><span class='line'>            }
</span><span class='line'>        }
</span><span class='line'>        return this.size;
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public Customer get(int rowIndex) {
</span><span class='line'>        //this way we ensure that we don't create several Customer instances 
</span><span class='line'>        //for the same id. Otherwise it would be confusing for beansbindings. 
</span><span class='line'>        Customer toReturn = null;
</span><span class='line'>        if (null != this.cache.get(rowIndex)) {
</span><span class='line'>            toReturn = this.cache.get(rowIndex).get();
</span><span class='line'>        }
</span><span class='line'>        if (null == toReturn) {
</span><span class='line'>            toReturn = getItem(rowIndex);
</span><span class='line'>            this.cache.put(rowIndex, new WeakReference&lt;Customer>(toReturn));
</span><span class='line'>        }
</span><span class='line'>
</span><span class='line'>        return toReturn;
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    private Customer getItem(final int j) {
</span><span class='line'>        final Customer customer = new Customer(j);
</span><span class='line'>
</span><span class='line'>
</span><span class='line'>        Runnable task = new SwingWorker() {
</span><span class='line'>
</span><span class='line'>            private String firstNameValue;
</span><span class='line'>            private String lastNameValue;
</span><span class='line'>
</span><span class='line'>            @Override
</span><span class='line'>            protected Object doInBackground() throws Exception {
</span><span class='line'>                //this is always executed in a different thread from the current thread
</span><span class='line'>                //it doesn't matter if the current thread is the EDT or a thread in the Executor
</span><span class='line'>                final java.sql.Statement stmt = connection.createStatement();
</span><span class='line'>                ResultSet executeQuery = stmt.executeQuery("SELECT ID, FIRST_NAME,LAST_NAME FROM CUSTOMERS ORDERDESC OFFSET " + j + " ROWS FETCH NEXT ROWS ONLY");
</span><span class='line'>                executeQuery.next();
</span><span class='line'>                firstNameValue = executeQuery.getString(2);
</span><span class='line'>                lastNameValue = executeQuery.getString(3);
</span><span class='line'>                return null;
</span><span class='line'>            }
</span><span class='line'>
</span><span class='line'>            @Override
</span><span class='line'>            protected void done() {
</span><span class='line'>                //this in the other hand will always be executed on the EDT.
</span><span class='line'>                //This has to be done in the EDT because currently JTableBinding
</span><span class='line'>                //is not smart enough to realize that the notification comes in another 
</span><span class='line'>                //thread and do a SwingUtilities.invokeLater. So we are force to execute this
</span><span class='line'>                // in the EDT. Seee http://markmail.org/thread/6ehh76zt27qc5fis and
</span><span class='line'>                // https://beansbinding.dev.java.net/issues/show_bug.cgi?id=60
</span><span class='line'>    
</span><span class='line'>                customer.setFirstName(firstNameValue);
</span><span class='line'>                customer.setLastName(lastNameValue);
</span><span class='line'>                Logger.getLogger(ResultListDerby.class.getName()).info("updating customer " + customer);
</span><span class='line'>            }
</span><span class='line'>        };
</span><span class='line'>
</span><span class='line'>        //NOTE that we don do task.execute()
</span><span class='line'>        //posting the task to an Executor gives more control on 
</span><span class='line'>        //how many threads are created. 
</span><span class='line'>        ex.execute(task);
</span><span class='line'>        return customer;
</span><span class='line'>    }
</span><span class='line'>}</span>

and Customer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<span class='line'>package jtablepagination;
</span><span class='line'>
</span><span class='line'>import java.beans.PropertyChangeListener;
</span><span class='line'>import java.beans.PropertyChangeSupport;
</span><span class='line'>import java.util.logging.Logger;
</span><span class='line'>
</span><span class='line'>/**
</span><span class='line'> *
</span><span class='line'> * @author Ruben Laguna &lt;ruben.laguna at gmail.com>
</span><span class='line'> */
</span><span class='line'>public class Customer {
</span><span class='line'>
</span><span class='line'>    private static final Logger LOG = Logger.getLogger(Customer.class.getName());
</span><span class='line'>    private static final String NOT_RETRIEVED_YET = "&lt;not retrieved yet>";
</span><span class='line'>    private int id = 0;
</span><span class='line'>    public static final String PROP_ID = "id";
</span><span class='line'>    protected String firstName = NOT_RETRIEVED_YET;
</span><span class='line'>    public static final String PROP_FIRSTNAME = "firstName";
</span><span class='line'>    protected String lastName = NOT_RETRIEVED_YET;
</span><span class='line'>    public static final String PROP_LASTNAME = "lastName";
</span><span class='line'>    private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
</span><span class='line'>
</span><span class='line'>    Customer(int j) {
</span><span class='line'>        this.id = j;
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public int getId() {
</span><span class='line'>        return id;
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public String getFirstName() {
</span><span class='line'>        return firstName;
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public void setFirstName(String firstName) {
</span><span class='line'>        String oldFirstName = this.firstName;
</span><span class='line'>        this.firstName = firstName;
</span><span class='line'>        propertyChangeSupport.firePropertyChange(PROP_FIRSTNAME, oldFirstName, firstName);
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public String getLastName() {
</span><span class='line'>        return lastName;
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public void setLastName(String lastName) {
</span><span class='line'>        String oldLastName = this.lastName;
</span><span class='line'>        this.lastName = lastName;
</span><span class='line'>        if (propertyChangeSupport.hasListeners(PROP_LASTNAME)) {
</span><span class='line'>            LOG.info("notifying!!!");
</span><span class='line'>        } else {
</span><span class='line'>            LOG.info("there is no listeners for the property");
</span><span class='line'>        }
</span><span class='line'>
</span><span class='line'>        propertyChangeSupport.firePropertyChange(PROP_LASTNAME, oldLastName, lastName);
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public void addPropertyChangeListener(PropertyChangeListener listener) {
</span><span class='line'>        LOG.info("add listener to customer " + this.id);
</span><span class='line'>        propertyChangeSupport.addPropertyChangeListener(listener);
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    public void removePropertyChangeListener(PropertyChangeListener listener) {
</span><span class='line'>        propertyChangeSupport.removePropertyChangeListener(listener);
</span><span class='line'>    }
</span><span class='line'>
</span><span class='line'>    @Override
</span><span class='line'>    public String toString() {
</span><span class='line'>        return "id: " + id + " firstName: " + firstName;
</span><span class='line'>    }
</span><span class='line'>}</span>

References

Comments

Copyright © 2015 - Ruben Laguna - Powered by Octopress