Groovy学习笔记之学霸分析仪
学霸分析仪
在Github上看源代码。
学霸分析仪概述
还是用上次笔记里面的jetty框架来写个学霸分析仪,功能如图:
首页有张列表列出了数据库里的学生信息,包括学号、姓名、性别以及班级。点击左边表头可以选中学生,然后点击 分析学员 按钮,可以进入到分析页面。
在学员分析页面,可以看到这个页面有学生的名字、班级、性别、学号以及各科考试的详细数据。并且点击考试右侧的 查看详情 按钮,会在下方显示出该同学在该场考试每门科目的具体分数,并配上一块雷达图显示各门分数的多少。
用到的技术
首先,
- 我们使用Groovy的Sql进行ddl和数据创建,然后数据库使用hsql。当然你可以很轻松地换成MySql、SQL Server以及Oracle啦。
- 后台有2个使用Groovy的jsonBuilder来暴露json格式数据的API,分别为
/api/exam.groovy
和/api/student.groovy
,分别为前台提供考试和学生信息供数据。 - 前台使用angularJs、bootstrap,雷达图使用chartJs实现。
本文重点会放在后台数据库上,不会讨论前台相关的问题。
DDL
首先,我们还是使用前篇文章提到的Jetty搭建服务器,所以有了app.groovy。然后,在里面手写SQL实现DDL。
像所有Java框架,执行SQL之前,首先需要有个dataSource。可以看到这里使用了hsqldb自带的JDBCDataSource,当然你也可以用譬如dbcp、c3p0或者光之cp之类的数据源池当做dataSource:
// ...
import org.hsqldb.jdbc.JDBCDataSource
// ...
def dataSource = new JDBCDataSource (
database: 'jdbc:hsqldb:file:magicMirrorDB',
user: 'sa',
password: ''
)
// ...
然后把这个dataSource保存到Servlet的context里面去,给后面的操作使用。
context.with {
// other settings...
setAttribute('dataSource', dataSource)
// other settings...
}
然后我们把这个dataSource包到Groovy.Sql里面去,然后使用Sql的方法来执行SQL。 譬如下面这段是创建Student表的DDL,可以看到这里我把整个DDL包到一个transaction的闭包里面去了,这样所有的ddl将属于同一个事务的操作。
// ...
Sql sql = new Sql(dataSource)
// ...
sql.withTransaction {
sql.execute '''
CREATE TABLE IF NOT EXISTS Student (
student_id INTEGER GENERATED BY DEFAULT AS IDENTITY,
student_no VARCHAR(50),
name VARCHAR(250),
gender VARCHAR(2),
class VARCHAR(50)
);
'''
// ... other ddls
}
sql.execute
后面跟的三个单引号是Groovy的字符串常量,可以在里面写很多从Java的角度看非法的字符串,譬如换行。这样写可以让代码整齐点。
CRUD
有了数据库Schema,就可以来操作数据了。譬如在/api/student.groovy
中,我们可以通过POST方法来创建或更新一个学生数据;GET方法来获取学生信息;DELETE方法来删除数据。先看POST方法:
import groovy.json.JsonSlurper
// ...
def sql = new Sql(application.dataSource)
// ...
if ('POST' == request.method) {
def slurper = new JsonSlurper()
def data = slurper.parse(request.inputStream)
def studentId
sql.withTransaction {
if (data.id != null) {
// ...省略update
} else {
def keys = sql.executeInsert """
INSERT INTO Student (student_no, name, gender, class) VALUES ($data.studentNo, $data.name, $data.gender, ${data['class']})
"""
studentId = keys[0][0]
application.fillExams.call(studentId, sql)
}
}
json {
response 'OK'
id studentId
}
}
// ...
首先我们从context中取出数据源dataSource并包装到Sql中。
然后如果request过来的方法是POST方法的话,使用groovy.json.JsonSlurper
读取request的body,解析为data。然后判断data是否含有id,如果没有id的情况下,则使用sql来执行insert语句。
可以看到这里用了3连双引号来包含SQL(GString),这样groovy会去做字符串插值(string interpolation),这是其一,其二sql.executeInsert
方法这边的字符串插值会被转换为PreparedStatement,不用担心SQL注入的问题(参考groovy.sql.Sql#Avoiding SQL injection)。
insert完成后,返回了自动生成的key,然后使用了之前提供的一个application.fillExams.call(studentId, sql)
方法来为该id的学生填充随机生成的数据。最后使用jsonBuilder返回ok和学生id。
更新学生数据和之前一样,区别在于这次post过来的数据里面有id号:
// ...
if ('POST' == request.method) {
// ...
sql.withTransaction {
if (data.id != null) {
studentId = data.id
sql.executeUpdate """
UPDATE Student set student_no = $data.studentNo, name = $data.name, gender = $data.gender, class = ${data['class']}
WHERE student_id = $studentId
"""
} else {
// ...
}
}
json {
response 'OK'
id studentId
}
}
// ...
然后我们来看看GET方法:
// ...
if ('GET' == request.method) {
if(params.studentId != null) {
def row = sql.firstRow("""
SELECT student_id, student_no, name, gender, class FROM Student WHERE student_id = ${params.studentId}
""")
json {
id row['student_id']
studentNo row['student_no']
name row.name
gender row.gender
'class' row['class']
}
} else {
def result = [];
sql.eachRow '''
SELECT student_id, student_no, name, gender, class FROM Student order by student_id
''', { row ->
result << [
id : row['student_id'],
studentNo: row['student_no'],
name : row.name,
gender : row.gender,
class : row['class']
]
}
json result
}
}
// ...
在request.method为get的时候,判断是否有studentId传过来,如果有的话,使用sql.firstRow
方法返回单行学生信息;如果没有的话,则使用sql.eachRow
方法,并且传入一个闭包操作每一行的数据,这里把每行数据加入一个result的list中去。最后使用jsonBuilder输出结果。
然后我们看看DELETE方法:
if ('DELETE' == request.method) {
def delCount = 0
if (params.id) {
delCount = sql.executeUpdate """
DELETE FROM Student where student_id = $params.id
"""
}
json {
response 'OK'
count "$delCount"
}
}
和想象中的一样,sql.executeUpdate
返回影响到的行数。
Batch操作
除了上述简单的CRUD之外,还可以用Batch来批量操作SQL。我们来看看DataGenerator.groovy
里面的fillExams
方法是如何为学生填充随机分数的。
// ...
class DataGenerator {
// ...
def exams = ['摸底考试', '分班考试', '期中考', '期末考', '模拟一', '模拟二', '模拟终', '高考']
// ...
def fillExams = { studentId, sql ->
def sIds = []
if(studentId != null) {
sIds << studentId;
} else {
sql.eachRow('SELECT student_id FROM Student') { sIds << it.student_id }
}
def insertExamSql = 'INSERT INTO Exam(student_id, subject_id, name, score, sort)VALUES(?, ?, ?, ?, ?)'
exams.eachWithIndex { examName, index ->
sIds.each { student ->
sql.eachRow('SELECT subject_id FROM Subject') { subject ->
sql.withBatch insertExamSql, { ps ->
ps.addBatch(student, subject['subject_id'], examName, randScore(), index)
}
}
}
}
}
// ...
}
实际上fillExams
不是一个方法而是一个闭包,这样fillExams
就可以作为方法对象来传递了。然后这个闭包接受2个参数:studentId和sql。其中studentId是学生的id号,sql则是sql操作对象。
fillExams
首先判断有无studentId,如果无,则针对数据库中所有的学生进行操作,如果有则针对当前这个学生进行操作。然后insertExamSql
定义了insert的语句,因为batch必须是PreparedStatement的,所以使用?
来传递参数。针对exams列表中的中每一个考试,循环每一个学生id,使用sql.eachRow
选出Subject表中每一个科目,并且针对每一个科目,使用sql.withBatch
方法和ps.addBatch
来增加batch。
嗯……其实我觉得吧,这里的sql.withBatch
,应该放到最外面才对,应该像这样写:
// ...
def fillExams = { studentId, sql ->
// ...
def insertExamSql = ...
sql.withBatch insertExamSql, { ps ->
exams.eachWithIndex { examName, index ->
sIds.each { student ->
sql.eachRow('SELECT subject_id FROM Subject') { subject ->
ps.addBatch(student, subject['subject_id'], examName, randScore(), index)
}
}
}
}
}
……好吧,反正能跑就行了,以后再改吧。
结论
好了本文到这里就大致介绍了Groovy的数据库操作的概况,但这只涵盖了学霸分析仪的一小部分,我不准备详细说自己看代码好了。看不懂也表问我了,这里是不负责任的讲解。
下一次讲如何把学霸分析仪改造成Spring框架的Groovy项目。
- 完 -
