JavaFX - SQL 查询的后台线程
Posted
技术标签:
【中文标题】JavaFX - SQL 查询的后台线程【英文标题】:JavaFX - Background Thread for SQL Query 【发布时间】:2021-12-08 21:32:36 【问题描述】:我想知道是否有人可以帮助我解决有关在 JavaFX 中创建后台线程的一个相当烦人的问题!我目前有几个 SQL 查询将数据添加到当前在 JavaFX 应用程序线程上运行的 UI(参见下面的示例)。但是,当每个查询执行时,它都会冻结 UI,因为它没有在后台线程上运行。我查看了各种使用 Task 的示例并理解了它们,但在执行数据库查询时我无法让它们工作,其中一些需要几秒钟才能运行。
这是执行查询的方法之一:
public void getTopOrders()
customerOrders.clear();
try
Connection con = DriverManager.getConnection(connectionUrl);
//Get all records from table
String SQL = "EXEC dbo.Get_Top_5_Customers_week";
ResultSet rs;
try (Statement stmt = con.createStatement();)
rs = stmt.executeQuery(SQL);
while (rs.next())
double orderValue = Double.parseDouble(rs.getString(3));
customerOrders.add(new CustomerOrders(rs.getString(1),
rs.getString(2), "£" + formatter.format(orderValue),
rs.getString(4).substring(6, 8) + "/" +
rs.getString(4).substring(4, 6) + "/" +
rs.getString(4).substring(0, 4)));
catch (SQLException | NumberFormatException e)
每个处理过的记录都被添加到一个 ObservableList 中,该 ObservableList 链接到一个 TableView 或图形,或者只是在标签上设置文本(取决于查询)。如何在后台线程上执行查询,并且仍然可以免费使用界面并从查询中更新
提前致谢
【问题讨论】:
你观察到 javafx.concurrent 包中的类了吗? docs.oracle.com/javafx/2/api/javafx/concurrent/…。我认为,Task 类对您来说应该特别有趣:docs.oracle.com/javafx/2/api/javafx/concurrent/Task.html,您可以阅读它的 javadoc,以了解完整的选项列表。 也许DataFX可以帮助你:javafxdata.org或guigarage.com/category/datafx 【参考方案1】:我创建了一个sample solution 用于使用Task(如Alexander Kirov 的评论中所建议的那样)来访问JavaFX 应用程序线程并发执行线程上的数据库。
示例解决方案的相关部分转载如下:
// fetches a collection of names from a database.
class FetchNamesTask extends DBTask<ObservableList<String>>
@Override protected ObservableList<String> call() throws Exception
// artificially pause for a while to simulate a long
// running database connection.
Thread.sleep(1000);
try (Connection con = getConnection())
return fetchNames(con);
private ObservableList<String> fetchNames(Connection con) throws SQLException
logger.info("Fetching names from database");
ObservableList<String> names = FXCollections.observableArrayList();
Statement st = con.createStatement();
ResultSet rs = st.executeQuery("select name from employee");
while (rs.next())
names.add(rs.getString("name"));
logger.info("Found " + names.size() + " names");
return names;
// loads a collection of names fetched from a database into a listview.
// displays a progress indicator and disables the trigge button for
// the operation while the data is being fetched.
private void fetchNamesFromDatabaseToListView(
final Button triggerButton,
final ProgressIndicator databaseActivityIndicator,
final ListView listView)
final FetchNamesTask fetchNamesTask = new FetchNamesTask();
triggerButton.setDisable(true);
databaseActivityIndicator.setVisible(true);
databaseActivityIndicator.progressProperty().bind(fetchNamesTask.progressProperty());
fetchNamesTask.setOnSucceeded(new EventHandler<WorkerStateEvent>()
@Override public void handle(WorkerStateEvent t)
listView.setItems(fetchNamesTask.getValue());
);
fetchNamesTask.runningProperty().addListener(new ChangeListener<Boolean>()
@Override public void changed(ObservableValue<? extends Boolean> observable, Boolean wasRunning, Boolean isRunning)
if (!isRunning)
triggerButton.setDisable(false);
databaseActivityIndicator.setVisible(false);
;
);
databaseExecutor.submit(fetchNamesTask);
private Connection getConnection() throws ClassNotFoundException, SQLException
logger.info("Getting a database connection");
Class.forName("org.h2.Driver");
return DriverManager.getConnection("jdbc:h2:~/test", "sa", "");
abstract class DBTask<T> extends Task<T>
DBTask()
setOnFailed(new EventHandler<WorkerStateEvent>()
@Override public void handle(WorkerStateEvent t)
logger.log(Level.SEVERE, null, getException());
);
// executes database operations concurrent to JavaFX operations.
private ExecutorService databaseExecutor = Executors.newFixedThreadPool(
1,
new DatabaseThreadFactory()
);
static class DatabaseThreadFactory implements ThreadFactory
static final AtomicInteger poolNumber = new AtomicInteger(1);
@Override public Thread newThread(Runnable runnable)
Thread thread = new Thread(runnable, "Database-Connection-" + poolNumber.getAndIncrement() + "-thread");
thread.setDaemon(true);
return thread;
请注意,一旦您开始同时执行操作,当一切都是单线程时,您的编码和 UI 会比没有任务的默认模式更复杂。例如,在我的示例中,我禁用了启动任务的按钮,因此您不能让多个任务在后台运行做同样的事情(这种处理类似于网络世界,您可能会禁用表单发布按钮以防止表格被双重张贴)。当长时间运行的数据库任务正在执行时,我还在场景中添加了一个动画进度指示器,以便用户可以看到正在发生的事情。
示例程序输出展示了长时间运行的数据库操作正在进行时的 UI 体验(请注意,在获取期间进度指示器正在动画,这意味着 UI 是响应式的,尽管屏幕截图没有显示这一点):
要比较具有并发任务的实现与在 JavaFX 应用程序线程上执行所有内容的实现的额外复杂性和功能,您可以查看another version of the same sample which does not use tasks。请注意,在我使用玩具本地数据库的情况下,基于任务的应用程序的额外复杂性是不必要的,因为本地数据库操作执行得非常快,但是如果您使用长时间运行的复杂查询连接到大型远程数据库,则基于任务的应用程序这种方法很有价值,因为它为用户提供了更流畅的 UI 体验。
【讨论】:
谢谢。我已经设法使用您的出色示例来允许我的数据库查询在后台运行,然后将项目添加到表视图中。不过我还有一个问题。为什么当我运行一个更改标签值的数据库查询时(使用 label.setText(rs.getString(x)); 它拒绝工作吗?我是否遗漏了一些简单的东西。我假设表一个有效,因为它只是添加数据到表视图然后使用的可观察列表中,但我不确定要使线程更改我的标签的方法? - 请参阅pastebin.com/TXyf00xq【参考方案2】:使用jewelsea提供的解决方案设法解决。值得注意的是,如果在不使用需要更新 UI 上的项目(例如文本字段或标签)的列表、表格和/或可观察列表时实现此方法,则只需在 Platform.runLater 中添加更新代码。下面是一些代码 sn-ps,显示了我的工作解决方案。
代码:
public void getSalesData()
try
Connection con = DriverManager.getConnection(connectionUrl);
//Get all records from table
String SQL = "EXEC dbo.Order_Information";
try (Statement stmt = con.createStatement(); ResultSet rs =
stmt.executeQuery(SQL))
while (rs.next())
todayTot = Double.parseDouble(rs.getString(7));
weekTot = Double.parseDouble(rs.getString(8));
monthTot = Double.parseDouble(rs.getString(9));
yearTot = Double.parseDouble(rs.getString(10));
yearTar = Double.parseDouble(rs.getString(11));
monthTar = Double.parseDouble(rs.getString(12));
weekTar = Double.parseDouble(rs.getString(13));
todayTar = Double.parseDouble(rs.getString(14));
deltaValue = Double.parseDouble(rs.getString(17));
yearPer = yearTot / yearTar * 100;
monthPer = monthTot / monthTar * 100;
weekPer = weekTot / weekTar * 100;
todayPer = todayTot / todayTar * 100;
//Doesn't update UI unless you add the update code to Platform.runLater...
Platform.runLater(new Runnable()
public void run()
todayTotal.setText("£" + formatter.format(todayTot));
weekTotal.setText("£" + formatter.format(weekTot));
monthTotal.setText("£" + formatter.format(monthTot));
yearTotal.setText("£" + formatter.format(yearTot));
yearTarget.setText("£" + formatter.format(yearTar));
monthTarget.setText("£" + formatter.format(monthTar));
weekTarget.setText("£" + formatter.format(weekTar));
todayTarget.setText("£" + formatter.format(todayTar));
yearPercent.setText(percentFormatter.format(yearPer) + "%");
currentDelta.setText("Current Delta (Week Ends): £"
+ formatter.format(deltaValue));
);
catch (SQLException | NumberFormatException e)
public void databaseThreadTester()
fetchDataFromDB();
private void fetchDataFromDB()
final testController.FetchNamesTask fetchNamesTask = new testController.FetchNamesTask();
databaseActivityIndicator.setVisible(true);
databaseActivityIndicator.progressProperty().bind(fetchNamesTask.progressProperty());
fetchNamesTask.setOnSucceeded(new EventHandler<WorkerStateEvent>()
@Override
public void handle(WorkerStateEvent t)
);
fetchNamesTask.runningProperty().addListener(new ChangeListener<Boolean>()
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean wasRunning, Boolean isRunning)
if (!isRunning)
databaseActivityIndicator.setVisible(false);
;
);
databaseExecutor.submit(fetchNamesTask);
abstract class DBTask<T> extends Task
DBTask()
setOnFailed(new EventHandler<WorkerStateEvent>()
@Override
public void handle(WorkerStateEvent t)
);
class FetchNamesTask extends testController.DBTask
@Override
protected String call() throws Exception
fetchNames();
return null;
private void fetchNames() throws SQLException, InterruptedException
Thread.sleep(5000);
getTopOrders();
getSalesData();
唯一似乎不适用于此实现的是以下内容,不知道为什么它不起作用但它没有绘制图表。
public void addCricketGraphData()
yearChart.getData().clear();
series.getData().clear();
series2.getData().clear();
try
Connection con = DriverManager.getConnection(connectionUrl);
//Get all records from table
String SQL = "...omitted...";
try (Statement stmt = con.createStatement(); ResultSet rs =
stmt.executeQuery(SQL))
while (rs.next())
Platform.runLater(new Runnable()
@Override
public void run()
try
series.getData().add(new XYChart.Data<String, Number>(rs.getString(1),
Double.parseDouble(rs.getString(7))));
series2.getData().add(new XYChart.Data<String, Number>(rs.getString(1),
Double.parseDouble(rs.getString(8))));
catch (SQLException ex)
Logger.getLogger(testController.class.getName()).log(Level.SEVERE, null, ex);
);
catch (SQLException | NumberFormatException e)
yearChart = createChart();
protected LineChart<String, Number> createChart()
final CategoryAxis xAxis = new CategoryAxis();
final NumberAxis yAxis = new NumberAxis();
// setup chart
series.setName("Target");
series2.setName("Actual");
xAxis.setLabel("Period");
yAxis.setLabel("£");
//Add custom node for each point of data on the line chart.
for (int i = 0; i < series2.getData().size(); i++)
nodeCounter = i;
final int value = series.getData().get(nodeCounter).getYValue().intValue();
final int value2 = series2.getData().get(nodeCounter).getYValue().intValue();
int result = value2 - value;
Node node = new HoveredThresholdNode(0, result);
node.toBack();
series2.getData().get(nodeCounter).setNode(node);
yearChart.getData().add(series);
yearChart.getData().add(series2);
return yearChart;
【讨论】:
以上是关于JavaFX - SQL 查询的后台线程的主要内容,如果未能解决你的问题,请参考以下文章
JavaFX 多线程之 TaskServiceScheduledService