目錄 [隱藏]
前言:
在開發 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_workspace\Letv\src\main\resources\db\letv.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_workspace\Letv\src\main\resources\db\letv.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");
});
未完待續 ……