前面学习的Kotlin知识只有通过项目贯穿起来,才能将书本中知识变成自己的。通过项目实战读者能够了解软件开发流程,了解所学知识在实际项目中使用的情况,哪些是重点的,哪些是了解的。
本章介绍通过Kotlin语言实现的PetStore宠物商店项目,所涉及的知识点:Kotlin面向对象、Lambda表达式、Swing、Exposed框架和数据库相关等知识,其中还会用到方方面面的Kotlin基础知识。
本节对PetStore宠物商店项目进行分析和设计,其中设计过程包括原型设计、数据库设计、架构设计和系统设计。
28.1.1 项目概述
PetStore是Sun(现在Oracle)公司为了演示自己的Java
EE技术,而编写的一个基于Web宠物店项目,如图28-1所示为项目启动页面,项目介绍网站是http://www.oracle.com/technetwork/Kotlin/index-136650.html。
PetStore是典型的电子商务项目,是现在很多电商平台的雏形。技术方面主要是Java EE技术,用户界面采用Java Web介绍实现。但本书介绍Kotlin语言,不介绍Java Web技术,所以本章的PetStore项目用户界面采用Kotlin + Java Swing技术实现。
28.1.2 需求分析
PetStore宠物商店项目主要功能如下:
用户登录
查询商品
添加商品到购物车
查看购物车
下订单
查看订单
采用用例分析函数描述用例图如图28-2所示。
28.1.3 原型设计
原型设计草图对于开发人员、设计人员、测试人员、UI设计人员以及用户都是非常重要的。PetStore宠物商店项目原型设计图如图28-3所示。
图28-3 PetStore宠物商店项目原型设计图
28.1.4 数据库设计
Sun提供的PetStore宠物商店项目数据库设计比较复杂,根据如图28-2的用例图重新设计数据库,数据库设计模型如图28-4所示。
数据库设计模型中各个表说明如下:
1.用户表
用户表(英文名accounts)是PetStore宠物商店的注册用户,用户Id(英文名userid)是主键,用户表结构如表28-1所示。
2.商品表
商品表(英文名products)是PetStore宠物商店所销售的商品(宠物),商品Id(英文名productid)是主键,商品表结构如表28-2所示。
3.订单表
订单表(英文名orders)记录了用户每次购买商品所生成的订单信息,订单Id(英文名orderid)是主键,订单表结构如表28-3所示。
4. 订单明细表
订单表中不能描述其中有哪些商品,购买商品的数量,购买时商品的单价等信息,这些信息被记录在订单明细表中。订单明细表(英文名ordersdetails),记录了某个订单更加详细的详细,订单明细表主键是由orderid和productid两个字段联合而成,是一种联合主键,订单明细表结构如表28-4所示。
从图28-4所示的数据库设计模型中可以编写DDL(数据定义)语句[1],使用这些语句可以很方便地创建和维护数据库中的表结构。
[1] 数据定义语句,用于创建、删除和修改数据库对象,包括DROP、CREATE、ALTER、GRANT、REVOKE和TRUNCATE等语句。
28.1.5 架构设计
无论是复杂的企业级系统,还是手机上的应用,都应该有效地组织程序代码,进而能提高开发效率、降低开发成本,这就需要设计。而架构设计就是系统的“骨架”,它是源自于前人经验的总结和提炼,形式一种模式推而广之。但是遗憾的是本书的定位是初学者,并不是介绍架构设计方面的书。为了开发PetStore宠物商店项目需要,这里笔者给出最简单的架构设计结果。
世界著名软件设计大师Martin Fowler在他《企业应用架构模式》(英文名Patterns of Enterprise Application Architecture)一书中提到,为了有效地组织代码,一个系统应该分为三个基本层,如图28-5所示。“层”(Layer)是相似功能的类和接口的集合,“层”之间是松耦合的,“层”的内部是高内聚的。
o 表示层。用户与系统交互的组件集合。用户通过这一层向系统提交请求或发出指令,系统通过这一层接收用户请求或指令,待指令消化吸收后再调用下一层,接着将调用结果展现到这一层。表示层应该是轻薄的,不应该具有业务逻辑。
o 服务层。系统的核心业务处理层。负责接收表示层的指令和数据,待指令和数据消化吸收后,再进行组织业务逻辑的处理,并将结果返回给表示层。
o 数据持久层。数据持久层用于访问持久化数据,持久化数据可以是保存在数据库、文件、其他系统或者网络数据。根据不同的数据来源,数据持久层会采用不同的技术,例如:如果数据保存到数据库中,则使用JDBC技术;如果数据保存JSON文件在,则需要I/O流和JSON解码技术实现。
Martin Fowler分层架构设计看起来像一个多层“蛋糕”,蛋糕师们在制作多层“蛋糕”的时候先做下层再做上层,最后做顶层。没有下层就没有上层,这叫作“上层依赖于下层”。为了降低松耦度,层之间还需要定义接口,通过接口隔离实现细节,上层调用者用只关心接口,不关心下一层的实现细节。
Martin Fowler分层架构是基本形式,在具体实现项目设计时,可能有所增加,也可能有所减少。本章实现的PetStore宠物商店项目,由于简化了需求,逻辑比较简单,可以不需要服务层,表示层可以直接访问数据持久层,如图28-6所示,表示层采用Swing技术实现,数据持久层采用JDBC技术实现。
28.1.6 系统设计
系统设计是在具体架构下的设计实现,PetStore宠物商店项目主要分为表示层和数据数据持久层。下面分别介绍一下它们的具体实现。
1.数据持久层设计
数据持久层在具体实现时,会采用DAO(数据访问对象)设计模式,数据库中每一个数据表,对应一个DAO对象,每一个DAO对象中有访问数据表的CRUD四类操作。
如图28-7所示PetStore宠物商店项目的数据持久层类图,首先定义了4个DAO接口,这4个接口对应数据中4个表,接口定义的函数是对数据库表的CRUD操作。
2.表示层
主要使用Swing技术,每一个界面就是一个窗口对象。在表示层中各个窗口是依据原型设计而来的。PetStore宠物商店项目表示层类如图28-8所示,其中有三个窗口类,LoginFrame用户登录窗口、CartFrame购物车窗口和ProductListFrame商品列表窗口,它们有共同的父类MyFrame,MyFrame类是根据自己的项目情况进行的封装,从类图中可见CartFrame与ProductListFrame具有关联关系,CartFrame包含一个对ProductListFrame的引用。
另外,CartFrame与ProductListFrame会使用到表格,所以自定义了两个表模型CartTableModel和ProductTableModel。
在设计完成之后,在编写Kotlin代码之前,应该创建数据库。
28.2.1 迭代1.1:安装和配置MySQL数据库
首先应该为开发该项目,准备好数据库。本书推荐使用MySQL数据库,如果没有安装MySQL数据库,可以参考25.1.1节安装MySQL数据库。
28.2.1 迭代1.2:编写数据库DDL脚本
按照图28-4所示的数据库设计模型编写数据库DDL脚本。当然,也可以通过一些工具生成DDL脚本,然后把这个脚本放在数据库中执行就可以了。下面是编写的DDL脚本:
/* 创建数据库 */
CREATE DATABASE IF NOT EXISTS petstore;
use petstore;
/* 用户表 /
CREATE TABLE IF NOT EXISTS accounts (
userid varchar(80) not null, / 用户Id /
password varchar(25) not null,
/ 用户密码 /
email
varchar(80) not null, /
用户Email /
name varchar(80) not null, / 用户名 /
addr varchar(80) not null, / 地址 /
city varchar(80) not null, / 所在城市 /
country varchar(20) not null, / 国家 /
phone varchar(80) not null, / 电话号码 */
PRIMARY KEY (userid));
/* 商品表 /
CREATE TABLE IF NOT EXISTS products (
produc tidvarchar(10) not null, / 商品Id /
category varchar(10) not null, / 商品类别 /
cname varchar(80) null, / 商品中文名 /
ename varchar(80) null, / 商品英文名 /
image varchar(20) null, / 商品图片 /
descn varchar(255) null, / 商品描述 /
listprice decimal(10,2) null, / 商品市场价 /
unitcost decimal(10,2) null, / 商品单价 */
PRIMARY KEY (productid));
/* 订单表 /
CREATE TABLE IF NOT EXISTS orders (
orderid bigint not null, / 订单Id /
userid varchar(80) not null, / 下订单的用户Id /
orderdate datetime not null, / 下订单时间 /
status int not null default 0, / 订单付款状态 0待付款 1已付款 /
amount decimal(10,2) not null, / 订单应付金额 */
PRIMARY KEY (orderid));
/* 订单明细表 /
CREATE TABLE IF NOT EXISTS orderdetails (
orderid bigint not null, / 订单Id /
productid varchar(10) not null, / 商品Id /
quantity int not null, / 商品数量 /
unitcost decimal(10,2) null, / 商品单价 */
PRIMARY KEY (orderid, productid));
如果读者对于编写DDL脚本不熟悉,可以直接使用笔者编写好的jpetstore-mysql-schema.sql脚本文件,文件位于PetStore项目下db目录中。
28.2.3 迭代1.3:插入初始数据到数据库
PetStore宠物商店项目有一些初始的数据,这些初始数据在创建数据库之后插入。这些插入数据的语句如下:
use petstore;
/* 用户表数据 */
INSERT INTO accounts VALUES(‘j2ee’,‘j2ee’,‘[email protected]’,‘关东升’, ‘北京丰台区’, ‘北京’, ‘中国’, ‘18811588888’);
INSERT INTO accounts
VALUES(‘ACID’,‘ACID’,‘[email protected]’,‘Tony’, ‘901 San Antonio Road’,
‘Palo Alto’, ‘USA’, ‘555-555-5555’);
/* 商品表数据 */
INSERT INTO products VALUES (‘FI-SW-01’,‘鱼类’,‘神仙鱼’, ‘Angelfish’, ‘fish1.jpg’, ‘来自澳大利亚的咸水鱼’,
650, 400);
INSERT INTO products VALUES (‘FI-SW-02’,‘鱼类’,‘虎鲨’, ‘Tiger Shark’,‘fish4.gif’, ‘来自澳大利亚的咸水鱼’,
850, 600);
…
INSERT INTO products VALUES (‘AV-CB-01’,‘鸟类’,‘亚马逊鹦鹉’, ‘Amazon Parrot’,‘bird4.gif’, ‘寿命长达75年的大鸟’, 3150, 3000);
INSERT INTO products VALUES (‘AV-SB-02’,‘鸟类’,‘雀科鸣鸟’, ‘Finch’,‘bird1.gif’, ‘会唱歌的鸟儿’, 150, 110);
如果读者不愿意自己编写插入数据的脚本文件,可以直接使用笔者编写好的jpetstore-mysql-dataload.sql脚本文件,文件位于PetStore项目下db目录中。
本项目推荐使用Intellij IDEA IDE工具,所以首先参考3.3节创建一个Intellij IDEA工具创建Kotlin+Gradle项目,项目名称PetStore。
28.3.1 迭代2.1:配置项目
PetStore项目创建完成后,需要配置Exposed框架,打开build.gradle文件,修改代码如下:
group “com.a51work6”
version ‘1.0-SNAPSHOT’
buildscript {
ext.kotlin_version = ‘1.1.60’
repositories {
mavenCentral()
}
dependencies {
classpath"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: ‘java’
apply plugin: ‘kotlin’
sourceCompatibility = 1.8
repositories {
mavenCentral()
maven { url “https://dl.bintray.com/kotlin/exposed” } ①
}
dependencies {
compile"org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile’org.jetbrains.exposed:exposed:0.9.1’ ②
compile(“mysql:mysql-connector-java:5.1.6”) ③
compile’org.slf4j:slf4j-api:1.7.25’ ④
compile’org.slf4j:slf4j-simple:1.7.25’ ⑤
testCompile group: ‘junit’, name: ‘junit’, version: ‘4.12’
}
compileKotlin {
kotlinOptions.jvmTarget = “1.8”
}
compileTestKotlin {
kotlinOptions.jvmTarget = “1.8”
}
在repositories部分中添加代码第①行,然后在dependencies部分中添加代码第②行~第⑤行。具体内容参考25.3节。
28.3.2 迭代2.2:添加资源图片
项目中会用到很多资源图片,为了打包发布项目方便,这些图片最好放到src源文件夹下,Intellij
IDEA会将该文件夹下有文件一起复制到字节码文件夹中。参考图28-9在resource文件夹下创建images文件夹,resource是资源目录,项目的所有资源文件(如:声音、图片和配置文件等)都要放到该目录下。然后将本章配套资源图片复制到项目的images文件夹下。
28.3.3 迭代2.3:添加包
参考图28-9在kotlin文件夹中创建如下4个包:
o com.a51work6.petstore.ui。放置表示层组件。
o com.a51work6.petstore.domain。放置实体类。
o com.a51work6.petstore.dao。放置数据持久层组件中DAO接口。
o com.a51work6.petstore.dao.mysql。放置数据持久层组件中DAO接口具体实现类,mysql说明是MySQL数据库DAO对象。该包中还放置了访问MySQL数据库一些辅助类和配置文件。
项目创建并初始化完成后,可以先编写数据持久层代码。
28.4.1 迭代3.1:编写实体类
无论是数据库设计还是面向对象的架构设计都会“实体”,“实体”是系统中的“人”、“事”、“物”等名词,如用户、商品、订单和订单明细等。在数据库设计时它将演变为表,如用户表(accounts)、商品表(products)、订单表(orders)和订单明细表(ordersdetails),在面向对象的架构设计时,实体将演变为“实体类”,如图28-10所示是PetStore宠物商店项目中的实体类,实体类属性与数据库表字段在是相似的,事实上它们描述的同一个事物,当然具有相同的属性,只是它们分别采用不同设计理念,实体类采用对象模型,表采用关系模式。
实体类没有别的操作,只有一些属性可以设计成为数据类。订单明细实体类OrderDetail代码如下:
//代码文件:
chapter28/PetStore/src/main/kotlin/com/a51work6/petstore/domain/OrderDetail.kt
package com.a51work6.petstore.domain
import java.math.BigDecimal
//订单明细
data class OrderDetail(
var orderid: Long = 0, // 订单Id
var productid: String? = null, // 商品Id
var quantity: Int = 0, // 商品数量
var unitcost: BigDecimal = BigDecimal(0) //单价
)
订单实体类Order的代码如下:
//代码文件:chapter28/PetStore/src/main/kotlin/com/a51work6/petstore/domain/Order.kt
package com.a51work6.petstore.domain
import java.math.BigDecimal
import java.util.*
data class Order(
var orderid: Long = 0, // 订单Id
var userid: String? = null, // 下订单的用户Id
var orderdate: Date? = null, // 下订单时间
var status:Int = 0, // 订单付款状态 0待付款 1已付款
var amount:BigDecimal = BigDecimal(0) // 订单应付金额
)
用户实体类Account的代码如下:
//代码文件:chapter28/PetStore/src/main/kotlin/com/a51work6/petstore/domain/Account.kt
package com.a51work6.petstore.domain
data class Account(
var userid: String? = null, // 用户Id
var username: String? = null, // 用户名
var password: String? = null, // 用户密码
var phone:String? = null, // 电话号码
var country: String? = null, //国家
var city:String? = null, // 所在城市
var addr:String? = null, // 地址
var email:String? = null // 用户Email
)
商品实体类Product的代码如下:
//代码文件:chapter28/PetStore/src/main/kotlin/com/a51work6/petstore/domain/Product.kt
package com.a51work6.petstore.domain
import java.math.BigDecimal
data class Product(
var productid: String? = null,// 商品Id
var category: String? = null,// 商品类别
var cname: String? = null,// 商品中文名
var ename:String? = null,// 商品英文名
var image:String? = null,// 商品描述
var descn:String? = null,// 商品描述
var listprice:BigDecimal = BigDecimal(0),// 商品市场价
var unitcost: BigDecimal = BigDecimal(0)// 商品单价
)
28.4.2 迭代3.2:创建数据表类
使用Exposed框架还需要编写与数据库对应的数据表类。具体实现代码如下:
//代码文件:chapter28/PetStore/src/main/kotlin/com/a51work6/petstore/dao/mysql/DBSchema.kt
package com.a51work6.petstore.dao.mysql
import org.jetbrains.exposed.sql.Table
const val URL =“jdbc:mysql://localhost:3306/petstore?useSSL=false&verifyServerCertificate=false”
const val DRIVER_CLASS = “com.mysql.jdbc.Driver”
const val DB_USER = “root”
const val DB_PASSWORD = “12345”
/* 用户表*/
object Accounts : Table() {
//声明表中字段
val userid = varchar(“userid”,length = 80).primaryKey()/* 用户Id /
val password = varchar(“password”,length = 25) / 用户密码 /
val email = varchar(“email”,length = 80) / 用户Email*/
val name = varchar(“name”, length= 80) /* 用户名*/
val addr = varchar(“addr”, length= 80) /* 地址*/
val city = varchar(“city”, length= 80) /* 所在城市*/
val country = varchar(“country”,length = 20) /* 国家*/
val phone = varchar(“phone”,length = 80) /* 电话号码*/
}
/* 商品表*/
object Products : Table() {
val productid =varchar(“productid”, length = 10).primaryKey() /* 商品Id*/
val category =varchar(“category”, length = 10) /* 商品类别 /
val cname = varchar(“cname”,length = 80) / 商品中文名*/
val ename = varchar(“ename”, length= 80) /* 商品英文名*/
val image = varchar(“image”,length = 20) /* 商品图片*/
val descn = varchar(“descn”,length = 255) /* 商品描述 /
val listprice =decimal(“listprice”, 10, 2) / 商品市场价 /
val unitcost =decimal(“unitcost”, 10, 2) / 商品单价*/
}
/* 订单表*/
object Orders : Table() {
val orderid = long(“orderid”).primaryKey() /* 订单Id*/
val userid = varchar(“userid”,length = 80) /* 下订单的用户Id*/
val orderdate =datetime(“orderdate”) /* 下订单时间*/
val status =integer(“status”)
/* 商品单价*/
val amount = decimal(“amount”,10, 2) /* 订单应付金额 */
}
/* 订单明细表*/
object Orderdetails : Table() {
val orderid =long(“orderid”).primaryKey() /* 订单Id*/
val productid =varchar(“productid”, length = 10).primaryKey()/* 商品Id*/
val quantity =integer(“quantity”) /* 商品数量*/
val unitcost =decimal(“unitcost”, 10, 2) /* 商品单价*/
}
上述代码表类结构与28.1.4节数据库设计模型表结构一致。
28.4.3 迭代3.3:编写DAO类
编写DAO类就没有实体类那么简单了,数据持久层开发的主要工作量主要是DAO类。图28-11是DAO实现类图。
1.用户管理DAO
用户管理AccountDao实现类AccountDaoImp代码如下:
//代码文件:chapter28/PetStore/src/main/kotlin/com/a51work6/petstore/dao/mysql/AccountDaoImp.kt
package com.a51work6.petstore.dao.mysql
import com.a51work6.petstore.dao.AccountDao
import com.a51work6.petstore.domain.Account
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.StdOutSqlLogger
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
//用户管理DAO
class AccountDaoImp : AccountDao {
override fun findAll(): List {
TODO(“not implemented”)
}
override fun findById(userid: String): Account? {
var accountList: List = emptyList()
//连接数据库
Database.connect(URL, user = DB_USER,
password = DB_PASSWORD,driver = DRIVER_CLASS)
//操作数据库
transaction
{
//添加SQL日志
logger.addLogger(StdOutSqlLogger)
//按照主键查询
accountList= Accounts.select { Accounts.userid.eq(userid) }.map {
val account = Account()
account.userid = it[Accounts.userid]
account.password = it[Accounts.password]
account.email = it[Accounts.email]
account.username = it[Accounts.name]
account.addr = it[Accounts.addr]
account.city = it[Accounts.city]
account.country = it[Accounts.country]
account.phone = it[Accounts.phone]
//Lambda表达式返回数据
account
}
}
return if (accountList.isEmpty()) null else accountList.first()
}
override fun create(account: Account) {
TODO("not implemented")
}
override fun modify(account: Account) {
TODO(“not implemented”)
}
override fun remove(account: Account) {
TODO(“not implemented”)
}
}
AccountDao接口中定义了5个抽象函数。但这些函数,在本项目中只需要实现findById函数。具体代码不再赘述。
2.商品管理DAO
商品管理ProductDao实现类ProductDaoImp代码如下:
//代码文件:chapter28/PetStore/src/main/kotlin/com/a51work6/petstore/dao/mysql/ProductDaoImp.kt
package com.a51work6.petstore.dao.mysql
import com.a51work6.petstore.dao.ProductDao
import com.a51work6.petstore.domain.Product
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.StdOutSqlLogger
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
class ProductDaoImp : ProductDao {
override fun findAll(): List {
var productList: List = emptyList()
//连接数据库
Database.connect(URL, user = DB_USER,
password = DB_PASSWORD, driver =DRIVER_CLASS)
//操作数据库
transaction
{
//添加SQL日志
logger.addLogger(StdOutSqlLogger)
productList = Products.selectAll().map {
val product = Product()
product.productid = it[Products.productid]
product.category = it[Products.category]
product.cname = it[Products.cname]
product.ename = it[Products.ename]
product.image = it[Products.image]
product.descn = it[Products.descn]
product.listprice = it[Products.listprice]
product.unitcost = it[Products.unitcost]
//Lambda表达式返回数据
product
}
}
return productList
}
override fun findById(productid: String): Product? {
var productList: List = emptyList()
//连接数据库
Database.connect(URL, user = DB_USER,
password= DB_PASSWORD, driver = DRIVER_CLASS)
//操作数据库
transaction
{
//添加SQL日志
logger.addLogger(StdOutSqlLogger)
//按照主键查询
productList = Products.select { Products.productid.eq(productid) }.map {
val product = Product()
product.productid = it[Products.productid]
product.category = it[Products.category]
product.cname = it[Products.cname]
product.ename = it[Products.ename]
product.image = it[Products.image]
product.descn = it[Products.descn]
product.listprice = it[Products.listprice]
product.unitcost =it[Products.unitcost]
//Lambda表达式返回数据
product
}
}
return if(productList.isEmpty()) null else productList.first()
}
override fun findByCategory(category: String): List {
var productList: List = emptyList()
//连接数据库
Database.connect(URL, user = DB_USER,
password
= DB_PASSWORD, driver = DRIVER_CLASS)
//操作数据库
transaction
{
//添加SQL日志
logger.addLogger(StdOutSqlLogger)
//按照主键查询
productList = Products.select { Products.category.eq(category) }.map {
val product = Product()
product.productid = it[Products.productid]
product.category = it[Products.category]
product.cname= it[Products.cname]
product.ename = it[Products.ename]
product.image = it[Products.image]
product.descn = it[Products.descn]
product.listprice = it[Products.listprice]
product.unitcost = it[Products.unitcost]
//Lambda表达式返回数据
product
}
}
return productList
}
override fun create(product: Product) {
TODO(“not implemented”)
}
override fun modify(product: Product) {
TODO(“not implemented”)
}
override fun
remove(product: Product) {
TODO(“not implemented”)
}
}
ProductDao接口中定义了6个抽象函数。但这些函数,在本项目中只需要实现findById、findAll、findByCategory和findById函数。
3.订单管理DAO
订单管理OrderDao实现类OrderDaoImp代码如下:
//代码文件:chapter28/PetStore/src/main/kotlin/com/a51work6/petstore/dao/mysql/OrderDaoImp.kt
package com.a51work6.petstore.dao.mysql
import com.a51work6.petstore.dao.OrderDao
import com.a51work6.petstore.domain.Order
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.StdOutSqlLogger
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.joda.time.DateTime
class OrderDaoImp : OrderDao {
override fun findAll(): List {
var orderList: List = emptyList()
//连接数据库
Database.connect(URL, user = DB_USER,
password
= DB_PASSWORD, driver = DRIVER_CLASS)
//操作数据库
transaction
{
//添加SQL日志
logger.addLogger(StdOutSqlLogger)
orderList = Orders.selectAll().map {
val order = Order()
order.orderid = it[Orders.orderid]
order.userid = it[Orders.userid]
order.orderdate = it[Orders.orderdate].toDate()
order.status = it[Orders.status]
order.amount = it[Orders.amount]
//Lambda表达式返回数据
order
}
}
return orderList
}
override fun findById(orderid: Int): Order? {
TODO(“not implemented”)
}
override funcreate(order: Order) {
//连接数据库
Database.connect(URL, user = DB_USER,
password
= DB_PASSWORD, driver = DRIVER_CLASS)
//操作数据库
transaction
{
//添加SQL日志
logger.addLogger(StdOutSqlLogger)
Orders.insert {
it[Orders.orderid] = order.orderid
it[Orders.userid] = order.userid!!
it[Orders.orderdate] = DateTime.now()//DateTime(order.orderdate)
it[Orders.status] = order.status
it[Orders.amount] = order.amount
}
}
}
override fun
modify(order: Order) {
TODO(“not implemented”)
}
override fun remove(order: Order) {
TODO(“not implemented”)
}
}
OrderDao接口中定义了5个抽象函数。但这些函数,在本项目中只需要实现findAll和create函数。
4.订单明细管理DAO
订单明细管理OrderDetailDao实现类OrderDetailDaoImp代码如下:
//代码文件:chapter28/PetStore/src/main/kotlin/com/a51work6/petstore/dao/mysql/OrderDetailDaoImp.kt
package com.a51work6.petstore.dao.mysql
import com.a51work6.petstore.dao.OrderDetailDao
import com.a51work6.petstore.domain.OrderDetail
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
class OrderDetailDaoImp : OrderDetailDao {
override funfindAll(): List {
TODO(“not implemented”)
}
override fun findByPK(orderid: Int, productid: String): OrderDetail? {
var orderDetailList: List = emptyList()
//连接数据库
Database.connect(URL, user = DB_USER,
password= DB_PASSWORD, driver = DRIVER_CLASS)
//操作数据库
transaction
{
//添加SQL日志
logger.addLogger(StdOutSqlLogger)
//按照主键查询
orderDetailList= Orderdetails.select {
Orderdetails.orderid.eq(orderid)
and Orderdetails.productid.eq(productid) }
.map {
val orderDetail = OrderDetail()
orderDetail.productid =it[Orderdetails.productid]
orderDetail.orderid = it[Orderdetails.orderid]
orderDetail.quantity = it[Orderdetails.quantity]
orderDetail.unitcost = it[Orderdetails.unitcost]
//Lambda表达式返回数据
orderDetail
}
}
return if
(orderDetailList.isEmpty()) null else orderDetailList.first()
}
override fun create(orderDetail: OrderDetail) {
//连接数据库
Database.connect(URL, user = DB_USER,
password = DB_PASSWORD, driver = DRIVER_CLASS)
//操作数据库
transaction
{
//添加SQL日志
logger.addLogger(StdOutSqlLogger)
Orderdetails.insert {
it[Orderdetails.orderid]= orderDetail.orderid
it[Orderdetails.productid]= orderDetail.productid!!
it[Orderdetails.quantity]= orderDetail.quantity
it[Orderdetails.unitcost] =orderDetail.unitcost
}
}
}
override fun modify(orderDetail: OrderDetail) {
TODO(“not implemented”)
}
override fun remove(orderDetail: OrderDetail) {
TODO(“not implemented”)
}
}
OrderDetailDao接口中定义了5个抽象函数。但这些函数,在本项目中只需要实现findByPK和create函数。
从客观上讲,表示层开发的工作量是很大的,有很多细节工作。
28.5.1 迭代4.1:编写启动类
Kotlin应用程序需要有一个具有main函数文件,它是项目入口,代码如下:
//代码文件:chapter28/PetStore/src/main/kotlin/com/a51work6/petstore/ui/MainApp.kt
package com.a51work6.petstore.ui
import com.a51work6.petstore.domain.Account//用户会话,用户登录成功后,保存当前用户信息
object UserSession { ①
var loginDate:
Date? = null ②
var account: Account? = null ③
}
fun main(args: Array) {
LoginFrame().isVisible = true
}
在main函数中实例化用户登录窗口——LoginFrame类。代码第①行声明了一个顶对象UserSession,UserSession是用户会话对象,当用户登录成功后,保存当前用户信息。声明为顶层对象,一是为了在整个项目中方便访问,二是声明对象是单例的能够在整个应用程序生命周期中保持状态。这样的事件是模拟Web应用开发中的会话(Session)对象,等用户打开浏览器,登录Web系统后,服务器端会将用户信息保存到会话对象中。
28.5.2 迭代4.2:编写自定义窗口类——MyFrame
由于Swing提供的JFrame类启动窗口后默认位于在屏幕的左上角,而本项目中所有的窗口都屏幕居中的,因此自定义了窗口类MyFrame,MyFrame代码如下:
//代码文件:chapter28/PetStore/src/main/kotlin/com/a51work6/petstore/ui/MyFrame.kt
package com.a51work6.petstore.ui
import java.awt.Toolkit
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.swing.JFrame
//这是一个屏幕居中的自定义窗口
open class MyFrame(title: String, width: Int, height:
Int) : JFrame(title) {
// 获得当前屏幕的宽
private val screenWidth = Toolkit.getDefaultToolkit().screenSize.getWidth() ①
// 获得当前屏幕的高
private val screenHeight = Toolkit.getDefaultToolkit().screenSize.getHeight() ②
init {
// 设置窗口大小
setSize(width, height)
// 计算窗口位于屏幕中心的坐标
val x =(screenWidth - width).toInt() / 2 ③
val y =(screenHeight - height).toInt() / 2 ④
// 设置窗口位于屏幕中心
setLocation(x, y)
// 注册窗口事件
addWindowListener(object : WindowAdapter() { ⑤
// 单击窗口关闭按钮时调用
override fun windowClosing(e: WindowEvent) {
// 退出系统
System.exit(0)
}
})
}
}
上述代码第①行和第②行是获取当前屏幕的宽和高,具体的计算过程,见代码第③行和第④行,具体的原理在24.5.7节已经介绍过程,这里不再赘述。
另外,代码第⑤行注册窗口事件,当用户单击窗口的关闭按钮时调用System.exit(0)语句退出系统,继承MyFrame类的所有窗口都可以单击关闭按钮时退出系统。
28.5.3 迭代4.3:用户登录窗口
main函数运行会启动用户登录窗口,界面如图28-12所示,界面中有一个文本框、一个密码框和两个按钮。用户输入账号和密码,单击“确定”按钮,如果输入的账号和密码正确,则登录成功进入商品列表窗口;如果输入的不正确,则弹出如图28-13所示的对话框。
用户登录窗口LoginFrame代码如下:
//代码文件:chapter28/PetStore/src/main/kotlin/com/a51work6/petstore/ui/LoginFrame.kt
package com.a51work6.petstore.ui
import com.a51work6.petstore.dao.mysql.AccountDaoImp ①
import java.awt.Font
import javax.swing.*
//用户登录窗口
class LoginFrame : MyFrame(“用户登录”, 400, 230) {
// private val txtAccountId: JTextField
// private val txtPassword: JPasswordField
init {
// 设置布局管理为绝对布局
layout =null
with(JLabel()) {
horizontalAlignment = SwingConstants.RIGHT
setBounds(51, 33, 83, 30)
text =“账号:”
font =Font(“微软雅黑”,Font.PLAIN, 15)
contentPane.add(this)
}
val
txtAccountId = JTextField(10).apply {
text =“j2ee”
setBounds(158, 33, 157, 30)
font =Font(“微软雅黑”,Font.PLAIN, 15)
contentPane.add(this)
}
with(JLabel()) {
text =“密码:”
font =Font(“微软雅黑”,Font.PLAIN, 15)
horizontalAlignment = SwingConstants.RIGHT
setBounds(51, 85, 83, 30)
contentPane.add(this)
}
val txtPassword = JPasswordField(10).apply {
text =“j2ee”
setBounds(158, 85, 157, 30)
contentPane.add(this)
}
val btnOk =JButton().apply {
text =“确定”
font =Font(“微软雅黑”,Font.PLAIN, 15)
setBounds(61, 140, 100, 30)
contentPane.add(this)
}
val btnCancel = JButton().apply {
text =“取消”
font =Font(“微软雅黑”,Font.PLAIN, 15)
setBounds(225, 140, 100, 30)
contentPane.add(this)
}
// 注册btnOk的ActionEvent事件监听器
btnOk.addActionListener { ②
UserSession.account= AccountDaoImp().findById(txtAccountId.text) ③
valpasswordText = String(txtPassword.password) ④
if (UserSession.account
!= null && passwordText == UserSession.account!!.password) { ⑤
println(“登录成功。”)
UserSession.loginDate= Date(
ProductListFrame().isVisible = true
isVisible = false
} else
{
val label = JLabel(“您输入的账号或密码有误,请重新输入。”)
label.font = Font(“微软雅黑”, Font.PLAIN, 15)
JOptionPane.showMessageDialog(null, label,
“登录失败”,
JOptionPane.ERROR_MESSAGE) ⑥
}
}
// 注册btnCancel的ActionEvent事件监听器
btnCancel.addActionListener { System.exit(0) } ⑦
}
}
上述代码第①行是创建AccountDaoImp对象,代码第③行是通过DAO对象调用findById函数,该函数是通过用户账号查询用户信息。
代码第②行是用户单击“确定”按钮调用的代码块。代码第④行从密码框中取出密码。代码第⑤行比较窗口中输入的密码与从数据库中查询的密码是否一致,如果一致则登录成功;否则登录失败。失败时弹出对话框,代码第⑥行JOptionPane.showMessageDialog函数可以弹出对话框,JOptionPane类其他类似的静态函数:
o showConfirmDialog。弹出确认框,可以Yes、No、Ok和Cancel等按钮。
o showInputDialog。弹出提示输入对话框。
o showMessageDialog。弹出消息提示对话框。
代码第⑥行showMessageDialog函数第一个参数是设置对话框所在的窗口,如果是当前窗口则设置为null,第二个参数要显示的消息,第三个参数是对话框标题,第四个参数要显示的消息类型,这些类型有:ERROR_MESSAGE、INFORMATION_MESSAGE、WARNING_MESSAGE、QUESTION_MESSAGE或PLAIN_MESSAGE,其中ERROR_MESSAGE是错误消息类型。