JavaFX 集成 Sqlite 和 Hibernate 開發爬蟲應用
- 2019 年 10 月 3 日
- 筆記
目錄 [隱藏]
前言:
在開發 JavaFX 應用總是避免不了數據存儲的,如果僅僅是一個簡單的配置數據,那麼不需要資料庫即可實現,那麼如果要面對幾十萬等大數據量的持久化存儲,那免不了要和資料庫和JDBC框架打交道了。
資料庫該怎麼選呢? 首先考慮我比較熟的 MySql,可是要使用MySql,你就必須要去官網下載MySql的安裝包,還要進行帳號密碼等配置,如果這軟體是面向大眾的,用戶要使用總不能還要先裝資料庫,再看半天安裝教程吧?
這不行,那麼我之前有接觸過兩個嵌入式資料庫,一個是H2,一個就是開發Android 時接觸的Sqlite。
H2 我事先考察了一下,覺得資料並不是很多,遠沒有 Sqlite 使用廣泛,而且 Sqlite 是 Android 官方內置的資料庫,我還去看了 Sqlite 最大數據存儲等測試文章,億級的數據量下還能保持性能,這才放心使用。
介面

Maven 環境
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>com.jfoenix</groupId> <artifactId>jfoenix</artifactId> <version>8.0.8</version> </dependency> <!--sqlite 版本3.7.2 --> <dependency> <groupId>org.xerial</groupId> <artifactId>sqlite-jdbc</artifactId> <version>3.7.2</version> </dependency> <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.21</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.21</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-1.2-api</artifactId> <version>2.8.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.11.3</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-core</artifactId> <version>4.1.21</version> </dependency> <!-- https://mvnrepository.com/artifact/io.datafx/flow --> <dependency> <groupId>io.datafx</groupId> <artifactId>flow</artifactId> <version>8.0.1</version> </dependency> <dependency> <groupId>org.controlsfx</groupId> <artifactId>controlsfx</artifactId> <version>8.40.14</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>5.4.2.Final</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.4</version> </dependency> <!-- https://mvnrepository.com/artifact/com.github.inamik.text.tables/inamik-text-tables --> <dependency> <groupId>com.github.inamik.text.tables</groupId> <artifactId>inamik-text-tables</artifactId> <version>0.8</version> </dependency>
項目結構
整合 Hibernate
Hibernate 並不支援 Sqlite,但只是缺少一個資料庫方言程式碼而已,這個在網上有很多,copy 一份在hibernate配置文件中引入就可以了。
SQLiteDialect.java 資料庫方言程式碼
package util; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.function.SQLFunctionTemplate; import org.hibernate.dialect.function.StandardSQLFunction; import org.hibernate.dialect.function.VarArgsSQLFunction; import org.hibernate.type.StandardBasicTypes; import java.sql.Types; public class SQLiteDialect extends Dialect { public SQLiteDialect() { super(); registerColumnType(Types.BIT, "integer"); registerColumnType(Types.TINYINT, "tinyint"); registerColumnType(Types.SMALLINT, "smallint"); registerColumnType(Types.INTEGER, "integer"); registerColumnType(Types.BIGINT, "bigint"); registerColumnType(Types.FLOAT, "float"); registerColumnType(Types.REAL, "real"); registerColumnType(Types.DOUBLE, "double"); registerColumnType(Types.NUMERIC, "numeric"); registerColumnType(Types.DECIMAL, "decimal"); registerColumnType(Types.CHAR, "char"); registerColumnType(Types.VARCHAR, "varchar"); registerColumnType(Types.LONGVARCHAR, "longvarchar"); registerColumnType(Types.DATE, "date"); registerColumnType(Types.TIME, "time"); registerColumnType(Types.TIMESTAMP, "timestamp"); registerColumnType(Types.BINARY, "blob"); registerColumnType(Types.VARBINARY, "blob"); registerColumnType(Types.LONGVARBINARY, "blob"); // registerColumnType(Types.NULL, "null"); registerColumnType(Types.BLOB, "blob"); registerColumnType(Types.CLOB, "clob"); registerColumnType(Types.BOOLEAN, "integer"); registerFunction("concat", new VarArgsSQLFunction(StandardBasicTypes.STRING, "", "||", "")); registerFunction("mod", new SQLFunctionTemplate(StandardBasicTypes.INTEGER, "?1 % ?2")); registerFunction("substr", new StandardSQLFunction("substr", StandardBasicTypes.STRING)); registerFunction("substring", new StandardSQLFunction("substr", StandardBasicTypes.STRING)); } public boolean supportsIdentityColumns() { return true; } public boolean hasDataTypeInIdentityColumn() { return false; } public String getIdentityColumnString() { return "integer"; } public String getIdentitySelectString() { return "select last_insert_rowid()"; } public boolean supportsLimit() { return true; } public String getLimitString(String query, boolean hasOffset) { return new StringBuffer(query.length() + 20).append(query).append(hasOffset ? " limit ? offset ?" : " limit ?") .toString(); } public boolean supportsTemporaryTables() { return true; } public String getCreateTemporaryTableString() { return "create temporary table if not exists"; } public boolean dropTemporaryTableAfterUse() { return false; } public boolean supportsCurrentTimestampSelection() { return true; } public boolean isCurrentTimestampSelectStringCallable() { return false; } public String getCurrentTimestampSelectString() { return "select current_timestamp"; } public boolean supportsUnionAll() { return true; } public boolean hasAlterTable() { return false; } public boolean dropConstraints() { return false; } public String getAddColumnString() { return "add column"; } public String getForUpdateString() { return ""; } public boolean supportsOuterJoinForUpdate() { return false; } public String getDropForeignKeyString() { throw new UnsupportedOperationException("No drop foreign key syntax supported by SQLiteDialect"); } public String getAddForeignKeyConstraintString(String constraintName, String[] foreignKey, String referencedTable, String[] primaryKey, boolean referencesPrimaryKey) { throw new UnsupportedOperationException("No add foreign key syntax supported by SQLiteDialect"); } public String getAddPrimaryKeyConstraintString(String constraintName) { throw new UnsupportedOperationException("No add primary key syntax supported by SQLiteDialect"); } public boolean supportsIfExistsBeforeTableName() { return true; } public boolean supportsCascadeDelete() { return false; } @Override public boolean bindLimitParametersInReverseOrder() { return true; } }
hibernate.cfg.xml Hibernate配置文件
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <property name="hibernate.dialect">util.SQLiteDialect</property> <!-- 資料庫方言 --> <property name="hibernate.connection.driver_class">org.sqlite.JDBC</property><!-- 引用jdbc包 --> <property name="hibernate.connection.url">jdbc:sqlite:D:eclipse_workspaceLetvsrcmainresourcesdbletv.db</property> <!-- 資料庫鏈接 --> <!-- <property name="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</property> <!– 資料庫方言 –>--> <!-- <property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property><!– 引用jdbc包 –>--> <!-- <property name="hibernate.connection.url">jdbc:mysql://127.0.0.1:3306/bbs?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL&useSSL=false&serverTimezone=CTT&nullCatalogMeansCurrent=true</property> <!– 資料庫鏈接 –>--> <!-- <property name="hibernate.connection.username">root</property>--> <!-- <property name="hibernate.connection.password">1234</property>--> <property name="hibernate.format_sql">true</property> <property name="hibernate.show_sql">true</property> <mapping resource="db/LetvConfigEntity.hbm.xml"/> <mapping resource="db/LetvCookieEntity.hbm.xml"/> <mapping resource="db/LetvLinkEntity.hbm.xml"/> <mapping resource="db/LetvUserEntity.hbm.xml"/> </session-factory> </hibernate-configuration>
這裡的 :<property name=”hibernate.connection.url”>jdbc:sqlite:D:eclipse_workspaceLetvsrcmainresourcesdbletv.db</property>
是絕對路徑,這個是用 idea 自動生成給我改了,可以使用相對路徑在項目根目錄下創建資料庫文件,而 url 只需要 : String url = “jdbc:sqlite:src/main/resources/db/letv.db”;
這樣寫就可以。
項目初始化連接資料庫自動建表:
資料庫那肯定不能沒有表,而表又不可能讓用戶去建,所以只能讓程式代勞,並不難,用 Navicat 打開資料庫建好表,導出 sql 文件,將其中的建表語句提取出來,在項目初次載入時,找到 sqlite 的 db 後綴的資料庫文件,如果沒有,那麼創建,連接資料庫,執行建表語句。
public class SqliteUtil { private static Connection connection; public synchronized static Connection getConnection() throws SQLException { //如果 當前練 if (connection == null) { try { String driverClass = "org.sqlite.JDBC"; Class.forName(driverClass); } catch (ClassNotFoundException e) { e.printStackTrace(); } String url = "jdbc:sqlite:src/main/resources/db/letv.db"; return connection = DriverManager.getConnection(url); } else { return connection; } } public static void close() { if (connection != null) { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } else { throw new NullPointerException("連接未開啟!"); } }
這段程式碼中,getConnection() 方法調用後會連接 Sqlite 資料庫,如果沒有,則會創建。
public static final String DB_USER = "create table letv_user (" + " letv_user_id integer(11) not null," + " letv_user_uid text(11)," + " letv_user_link text(40)," + " primary key (letv_user_id)" + ");"; public static void createDatabases() throws SQLException, IOException { Connection connection = getConnection(); Statement statement = connection.createStatement(); statement.execute(DB_CONFIG); statement.execute(DB_COOKIE); statement.execute(DB_LINK); statement.execute(DB_USER); close(); }
定義建表語句,調用 createDatabases() 則會執行建表語句創建表。
程式初次運行創建資料庫和表
if (this.getClass().getResource("db/letv.db") == null) { FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/Loading.fxml")); AnchorPane pane = loader.load(); Scene scene = new Scene(pane, 500, 300); PleaseProvideController loadController = loader.getController(); loadController.setInfo("正在創建資料庫……"); primaryStage.setScene(scene); primaryStage.initStyle(StageStyle.UNDECORATED); primaryStage.setAlwaysOnTop(true); primaryStage.getIcons().addAll(new Image("file:/resource/logo/logo.ico")); primaryStage.show(); Platform.runLater(() -> { try { SqliteUtil.createDatabases(); logger.info("資料庫創建成功!"); primaryStage.close(); start(new Stage()); } catch (SQLException | IOException e) { e.printStackTrace(); } }); }
創建資料庫需要一些時間,這個時候可以給一個 Loading 介面:
經過測試發現創建資料庫和表時間並不長,所以這一步可以省略,當然如果表多那就看情況了。
JFoenix 介面開發
JFoenix 的介面非常好看,Google Material 的設計風格,案例:
這個 UI 庫是開源的,非常美觀,提供的控制項也很豐富,只是文檔感覺不是很好,但好在可以再官方提供的 Demo 案例查看控制項的使用。
Github 地址 :
https://github.com/jfoenixadmin/JFoenix
官方文檔 :
http://www.jfoenix.com/documentation.html
JFoenix 表格 TreeTable
官方案例:

如果覺得 JFoenix 的表格實現程式碼要比原生的簡單,那你就錯了,程式碼量依舊比較大,而且如果需要對錶的列綁定欄位,欄位不是只讀,就是如果你需要表的欄位可以被編輯操作,那麼相應的綁定的欄位類型必須是 JavaFX 提供的 Property 類型,JavaFX 為這種類型提供了綁定方法,但是如果是使用這種類型去結合 Hibernate 欄位映射,報錯沒跑了。
所以,我只能將用戶映射表的實體類和綁定表的類分開,分為兩個類,一個欄位類型是原生的,另一個欄位類型是 Property 類型。
public class LetvCookieTable extends RecursiveTreeObject<LetvCookieTable> { public long cookieId; public StringProperty cookieKey; public StringProperty cookieValue; public LetvCookieTable(long cookieId,String cookieKey, String cookieValue) { this.cookieId = cookieId; this.cookieKey = new SimpleStringProperty(cookieKey); this.cookieValue = new SimpleStringProperty(cookieValue); }
這個就是用來綁定表的實體類,再表格介面載入的時候,查詢返回實體類結果集,接著將實體類轉換成 Property 類型的類添加到 ObservableList 中。
欄位綁定
//column JFXTreeTableColumn<LetvCookieTable, String> key = new JFXTreeTableColumn<>("Key"); key.setCellValueFactory((TreeTableColumn.CellDataFeatures<LetvCookieTable, String> param) -> { if (key.validateValue(param)) { return param.getValue().getValue().cookieKey; } else { return key.getComputedValue(param); } }); JFXTreeTableColumn<LetvCookieTable, String> value = new JFXTreeTableColumn<>("Value"); value.setCellValueFactory((TreeTableColumn.CellDataFeatures<LetvCookieTable, String> param) -> { if (value.validateValue(param)) { return param.getValue().getValue().cookieValue; } else { return value.getComputedValue(param); } });
TreeTable 綁定刪除按鈕
現在需要一個刪除的列,提供刪除按鈕,點擊後刪除這一行的數據。
程式碼和 TableView 大體上是一樣的,但在取值上有點小差異。
JFXTreeTableColumn<LetvCookieTable, String> options = new JFXTreeTableColumn<>("options"); options.setCellFactory(new Callback<TreeTableColumn<LetvCookieTable, String>, TreeTableCell<LetvCookieTable, String>>() { @Override public TreeTableCell<LetvCookieTable, String> call(TreeTableColumn<LetvCookieTable, String> param) { JFXButton button = new JFXButton(); button.setText("刪除"); return new TreeTableCell<LetvCookieTable, String>() { JFXButton delBtn = button; @Override protected void updateItem(String item, boolean empty) { super.updateItem(item, empty); if (empty) { setGraphic(null); setText(null); } else { delBtn.setOnMouseClicked(event -> { Transaction transaction = session.beginTransaction(); logger.info("刪除:" + getIndex()); LetvCookieTable table = getTreeTableView().getTreeItem(getIndex()).getValue(); session.doWork(connection -> { Statement st; logger.info("id:" + table.cookieId); String sql = "delete from letv_cookie where cookie_id = " + table.cookieId; st = connection.createStatement(); st.executeUpdate(sql); st.close(); }); NotificationUtil.notification("資訊", "刪除成功", "info"); transaction.commit(); observableCookie.remove(getIndex()); }); setGraphic(button); setText(null); } } }; } });
JavaFX 獲取當前行 :
getTableView().getItems().get(getIndex())
JFoenix :
getTreeTableView().getTreeItem(getIndex()).getValue()
TreeTable 可編輯
key.setCellFactory((TreeTableColumn<LetvCookieTable, String> param) -> new GenericEditableTreeTableCell<>( new TextFieldEditorBuilder())); key.setOnEditCommit((TreeTableColumn.CellEditEvent<LetvCookieTable, String> t) -> { LetvCookieTable table = t.getTreeTableView().getTreeItem(t.getTreeTablePosition() .getRow()) .getValue(); table.cookieKey.set(t.getNewValue()); Transaction transaction = session.beginTransaction(); Query updateLink = session.createQuery("update db.LetvCookieEntity set cookieKey = :newVal where cookieId=" + table.cookieId); updateLink.setParameter("newVal", t.getNewValue()); updateLink.executeUpdate(); transaction.commit(); session.clear(); NotificationUtil.notification("資訊","更新成功!","info"); });
未完待續 ……