目录 [隐藏]
- 0.1 前言:
- 0.2 界面
- 0.3 Maven 环境
- 0.4 项目结构
- 0.5 整合 Hibernate
- 0.5.1 SQLiteDialect.java 数据库方言代码
- 0.5.2 hibernate.cfg.xml Hibernate配置文件
- 0.6 项目初始化连接数据库自动建表:
- 0.6.1 程序初次运行创建数据库和表
- 0.7 JFoenix 界面开发
- 1 JFoenix 表格 TreeTable
-
- 1.0.1 字段绑定
- 1.0.2 TreeTable 绑定删除按钮
- 1.0.3 TreeTable 可编辑
-
前言:
在开发 JavaFX 应用总是避免不了数据存储的,如果仅仅是一个简单的配置数据,那么不需要数据库即可实现,那么如果要面对几十万等大数据量的持久化存储,那免不了要和数据库和JDBC框架打交道了。
数据库该怎么选呢? 首先考虑我比较熟的 MySql,可是要使用MySql,你就必须要去官网下载MySql的安装包,还要进行账号密码等配置,如果这软件是面向大众的,用户要使用总不能还要先装数据库,再看半天安装教程吧?
这不行,那么我之前有接触过两个嵌入式数据库,一个是H2,一个就是开发Android 时接触的Sqlite。
H2 我事先考察了一下,觉得资料并不是很多,远没有 Sqlite 使用广泛,而且 Sqlite 是 Android 官方内置的数据库,我还去看了 Sqlite 最大数据存储等测试文章,亿级的数据量下还能保持性能,这才放心使用。
界面
Maven 环境
<dependencies>
<dependency>
<groupId>junitgroupId> <artifactId>junitartifactId> <version>3.8.1version> <scope>testscope> dependency> <dependency> <groupId>com.jfoenixgroupId> <artifactId>jfoenixartifactId> <version>8.0.8version> dependency> <dependency> <groupId>org.xerialgroupId> <artifactId>sqlite-jdbcartifactId> <version>3.7.2version> dependency> <dependency> <groupId>org.slf4jgroupId> <artifactId>slf4j-apiartifactId> <version>1.7.21version> dependency> <dependency> <groupId>org.slf4jgroupId> <artifactId>slf4j-log4j12artifactId> <version>1.7.21version> dependency> <dependency> <groupId>org.apache.logging.log4jgroupId> <artifactId>log4j-1.2-apiartifactId> <version>2.8.2version> dependency> <dependency> <groupId>junitgroupId> <artifactId>junitartifactId> <version>4.12version> <scope>testscope> dependency> <dependency> <groupId>org.jsoupgroupId> <artifactId>jsoupartifactId> <version>1.11.3version> dependency> <dependency> <groupId>cn.hutoolgroupId> <artifactId>hutool-coreartifactId> <version>4.1.21version> dependency> <dependency> <groupId>io.datafxgroupId> <artifactId>flowartifactId> <version>8.0.1version> dependency> <dependency> <groupId>org.controlsfxgroupId> <artifactId>controlsfxartifactId> <version>8.40.14version> dependency> <dependency> <groupId>org.hibernategroupId> <artifactId>hibernate-coreartifactId> <version>5.4.2.Finalversion> dependency> <dependency> <groupId>org.projectlombokgroupId> <artifactId>lombokartifactId> <version>1.18.4version> dependency> <dependency> <groupId>com.github.inamik.text.tablesgroupId> <artifactId>inamik-text-tablesartifactId> <version>0.8version> 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配置文件
<hibernate-configuration>
<session-factory> <property name="hibernate.dialect">util.SQLiteDialectproperty> <property name="hibernate.connection.driver_class">org.sqlite.JDBCproperty> <property name="hibernate.connection.url">jdbc:sqlite:D:\eclipse_workspace\Letv\src\main\resources\db\letv.dbproperty> <property name="hibernate.format_sql">trueproperty> <property name="hibernate.show_sql">trueproperty> <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>
这里的 :
是绝对路径,这个是用 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 key = new JFXTreeTableColumn<>("Key");
key.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { if (key.validateValue(param)) { return param.getValue().getValue().cookieKey; } else { return key.getComputedValue(param); } }); JFXTreeTableColumn value = new JFXTreeTableColumn<>("Value"); value.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { if (value.validateValue(param)) { return param.getValue().getValue().cookieValue; } else { return value.getComputedValue(param); } });
TreeTable 绑定删除按钮
现在需要一个删除的列,提供删除按钮,点击后删除这一行的数据。
代码和 TableView 大体上是一样的,但在取值上有点小差异。
JFXTreeTableColumnString> options = new JFXTreeTableColumn<>("options");
options.setCellFactory(new CallbackString>, TreeTableCellString>>() { @Override public TreeTableCellString> call(TreeTableColumnString> param) { JFXButton button = new JFXButton(); button.setText("删除"); return new TreeTableCellString>() { 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 param) -> new GenericEditableTreeTableCell<>(
new TextFieldEditorBuilder()));
key.setOnEditCommit((TreeTableColumn.CellEditEvent 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"); });
未完待续 ……