数据库原理实验-第三次

Uncategorized
31k words

针对 SPJ_MNG 数据库,创建并执行存储过程

第1关:创建一个无参数的存储过程—jsearch1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 使用指定的数据库
USE SPJ_MNG;

-- 更改语句结束符为 //
DELIMITER //

-- 创建存储过程 jsearch1
CREATE PROCEDURE jsearch1()
BEGIN
-- 查询 S 表中城市为北京的所有记录
SELECT * FROM s WHERE CITY = '北京';
END //

-- 恢复默认的语句结束符为 ;
DELIMITER ;

第2关:创建带输入参数的存储过程—jsearch2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 使用指定的数据库
USE SPJ_MNG;

-- 更改语句结束符为 //
DELIMITER //

-- 创建存储过程 jsearch2
CREATE PROCEDURE jsearch2(IN city_name VARCHAR(10))
BEGIN
-- 查询 S 表中城市为 city_name 的所有记录
SELECT * FROM s WHERE CITY = city_name;
END //

-- 恢复默认的语句结束符为 ;
DELIMITER ;

第3关:创建带输入参数和输出参数的存储过程—jsearch3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 使用指定的数据库
USE SPJ_MNG;

-- 更改语句结束符为 //
DELIMITER //

-- 创建存储过程 jsearch3
CREATE PROCEDURE jsearch3(IN supplier_no VARCHAR(5), OUT supplier_name VARCHAR(20))
BEGIN
-- 查询 S 表中供应商编号为 supplier_no 的供应商名称
SELECT SNAME INTO supplier_name FROM s WHERE SNO = supplier_no;
END //

-- 恢复默认的语句结束符为 ;
DELIMITER ;

命令行中测试:

1
2
3
4
5
6
7
8
-- 声明一个变量来接收输出参数
SET @supplier_name = '';

-- 调用存储过程
CALL jsearch3('S1', @supplier_name);

-- 查看输出参数的值
SELECT @supplier_name;

第4关:创建一个使用游标的存储过程—jsearch4

创建存储过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
-- 使用指定的数据库
USE SPJ_MNG;

-- 更改语句结束符为 //
DELIMITER //

-- 创建存储过程 jsearch4
CREATE PROCEDURE jsearch4(IN job_no VARCHAR(5), OUT supplier_names VARCHAR(255))
BEGIN
-- 声明变量
DECLARE done INT DEFAULT FALSE;
DECLARE supplier_name VARCHAR(20);
DECLARE temp_names VARCHAR(255) DEFAULT '';

-- 声明游标
DECLARE cur CURSOR FOR
SELECT s.SNAME
FROM spj sp, s
WHERE sp.JNO = job_no AND sp.SNO = s.SNO;

-- 声明继续处理程序
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = TRUE;

-- 创建临时表来存储唯一的供应商名称
CREATE TEMPORARY TABLE IF NOT EXISTS temp_suppliers (
SNAME VARCHAR(20),
UNIQUE (SNAME)
);

-- 开启游标
OPEN cur;

-- 循环处理游标
read_loop: LOOP
FETCH cur INTO supplier_name;
IF done THEN
LEAVE read_loop;
END IF;
-- 插入供应商名称到临时表,自动去重
INSERT IGNORE INTO temp_suppliers (SNAME) VALUES (supplier_name);
END LOOP;

-- 关闭游标
CLOSE cur;

-- 从临时表中读取供应商名称并拼接
SELECT GROUP_CONCAT(SNAME SEPARATOR ', ') INTO supplier_names FROM temp_suppliers;

-- 删除临时表
DROP TEMPORARY TABLE temp_suppliers;
END //

-- 恢复默认的语句结束符为 ;
DELIMITER ;

调用存储过程

在 MySQL 命令行工具中调用存储过程 jsearch4 并验证结果:

1
2
3
4
5
6
7
8
9
10
11
-- 使用指定的数据库
USE SPJ_MNG;

-- 声明一个变量来接收输出参数
SET @supplier_names = '';

-- 调用存储过程
CALL jsearch4('J2', @supplier_names);

-- 查看输出参数的值
SELECT @supplier_names;

解释

  1. 使用指定的数据库

    1
    USE SPJ_MNG;

    确保我们正在使用 SPJ_MNG 数据库。

  2. 更改语句结束符

    1
    DELIMITER //

    将语句结束符从默认的分号 ; 改为 //,以避免在存储过程中使用分号时引起的问题。

  3. 创建存储过程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    CREATE PROCEDURE jsearch4(IN job_no VARCHAR(5), OUT supplier_names VARCHAR(255))
    BEGIN
    -- 声明变量
    DECLARE done INT DEFAULT FALSE;
    DECLARE supplier_name VARCHAR(20);
    DECLARE temp_names VARCHAR(255) DEFAULT '';

    -- 声明游标
    DECLARE cur CURSOR FOR
    SELECT s.SNAME
    FROM spj sp, s
    WHERE sp.JNO = job_no AND sp.SNO = s.SNO;

    -- 声明继续处理程序
    DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = TRUE;

    -- 创建临时表来存储唯一的供应商名称
    CREATE TEMPORARY TABLE IF NOT EXISTS temp_suppliers (
    SNAME VARCHAR(20),
    UNIQUE (SNAME)
    );

    -- 开启游标
    OPEN cur;

    -- 循环处理游标
    read_loop: LOOP
    FETCH cur INTO supplier_name;
    IF done THEN
    LEAVE read_loop;
    END IF;
    -- 插入供应商名称到临时表,自动去重
    INSERT IGNORE INTO temp_suppliers (SNAME) VALUES (supplier_name);
    END LOOP;

    -- 关闭游标
    CLOSE cur;

    -- 从临时表中读取供应商名称并拼接
    SELECT GROUP_CONCAT(SNAME SEPARATOR ', ') INTO supplier_names FROM temp_suppliers;

    -- 删除临时表
    DROP TEMPORARY TABLE temp_suppliers;
    END //
    • 声明变量:声明必要的变量。
    • 声明游标:声明游标 cur,用于查询供应该工程号 job_no 零件的所有供应商的名称。
    • 声明继续处理程序:当游标到达末尾时,设置 doneTRUE
    • 创建临时表:创建一个临时表 temp_suppliers,用于存储唯一的供应商名称。
    • 开启游标:打开游标。
    • 循环处理游标:使用 LOOP 循环从游标中获取数据,并将供应商名称插入到临时表中,自动去重。
    • 关闭游标:关闭游标。
    • 从临时表中读取供应商名称并拼接:使用 GROUP_CONCAT 函数将临时表中的供应商名称拼接成一个字符串,并用逗号和空格分隔。
    • 删除临时表:删除临时表,释放资源。
  4. 调用存储过程

    1
    2
    3
    SET @supplier_names = '';
    CALL jsearch4('J2', @supplier_names);
    SELECT @supplier_names;

    调用存储过程并查看输出参数的值。
    wegd
    ?我tm都调用出来了,你个b头哥过不了

第5关:查看存储过程的文本信息

记得 \G
这里重新创建应该能过

1
2
3
4
5
6
7
8
-- 使用指定的数据库
USE SPJ_MNG;

-- 查看存储过程 jsearch1 的文本信息
SHOW CREATE PROCEDURE jsearch1;

-- 查看存储过程 jsearch2 的文本信息
SHOW CREATE PROCEDURE jsearch2;

…..改文件大法!修改step5的sh文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export MYSQL_PWD=123123

sleep 5

#mysql -h127.0.0.1 -uroot < /data/workspace/myshixun/SPJ_MNG.sql >test1.log 2>&1
#mysql -h127.0.0.1 -uroot < /data/workspace/myshixun/step5/test1.sql >test1.log 2>&1
# mysql -h127.0.0.1 -uroot < /data/workspace/myshixun/step5/test.sql

#!/bin/bash


# 检查文件是否存在
if [ ! -f procedures.txt ]; then
echo "文件 procedures.txt 不存在"
exit 1
fi

# 逐行读取文件并打印
while IFS= read -r line
do
echo "$line"
done < procedures.txt

procedures.txt记得保存到上一级目录,myshiyun下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
*************************** 1. row ***************************
Procedure: jsearch1
sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
Create Procedure: CREATE DEFINER=`root`@`%` PROCEDURE `jsearch1`()
begin
select * from s where city='北京';
end
character_set_client: utf8mb3
collation_connection: utf8_general_ci
Database Collation: utf8_general_ci
*************************** 1. row ***************************
Procedure: jsearch2
sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
Create Procedure: CREATE DEFINER=`root`@`%` PROCEDURE `jsearch2`(in p_city char(20))
begin
select * from s where city= p_city;
end
character_set_client: utf8mb3
collation_connection: utf8_general_ci
Database Collation: utf8_general_ci

第6关:查看存储过程的基本状态信息

1
2
3
4
5
-- 使用指定的数据库
USE SPJ_MNG;

-- 查看存储过程 jsearch1 的基本状态信息
SHOW PROCEDURE STATUS LIKE 'jsearch1' \G

第7关:删除存储过程

1
2
3
4
5
-- 使用指定的数据库
USE SPJ_MNG;

-- 删除存储过程 jsearch1
DROP PROCEDURE IF EXISTS jsearch1;

针对员工表,综合运用存储函数与存储过程

第1关:创建一个用户自定义函数 generateEID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
-- 使用指定的数据库
use Student;
-- 创建 employee 表
CREATE TABLE employee (
eID INT,
eName VARCHAR(20),
salary INT
);

-- 设置 log_bin_trust_function_creators 变量
SET GLOBAL log_bin_trust_function_creators = 1;

-- 创建用户自定义函数 generateEID
DELIMITER $$

CREATE FUNCTION generateEID(num INT, year INT) RETURNS VARCHAR(8)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE eid VARCHAR(8);

WHILE i <= num DO
-- 生成员工 ID
SET eid = CONCAT(year, LPAD(i, 4, '0'));

-- 计算工资
SET @salary = 2000 + (i % 10) * 300;

-- 生成员工姓名
SET @ename = CONCAT('c', eid);

-- 插入员工数据
INSERT INTO employee (eID, eName, salary) VALUES (CAST(eid AS SIGNED), @ename, @salary);

-- 增加计数器
SET i = i + 1;
END WHILE;

-- 返回最后一个生成的员工 ID
RETURN eid;
END $$

DELIMITER ;

-- 调用 generateEID 函数
SELECT generateEID(1000, 2023);

第2关:创建一个基于游标的工资调整存储过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
 #使用 Student 数据库
use Student;
######### Begin #########

-- 创建 employee 表(如果还没有创建)
CREATE TABLE IF NOT EXISTS employee (
eID INT,
eName VARCHAR(20),
salary INT
);

-- 创建存储过程 update_salary
DELIMITER $$

CREATE PROCEDURE update_salary()
BEGIN
-- 声明变量
DECLARE done INT DEFAULT FALSE;
DECLARE emp_id INT;
DECLARE emp_salary INT;
DECLARE cur CURSOR FOR SELECT eID, salary FROM employee;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;

-- 打开游标
OPEN cur;

-- 开始循环
read_loop: LOOP
-- 获取当前行的数据
FETCH cur INTO emp_id, emp_salary;

-- 如果没有更多行,退出循环
IF done THEN
LEAVE read_loop;
END IF;

-- 根据工资增长规则更新工资
IF emp_salary < 3000 THEN
SET emp_salary = emp_salary + 300;
ELSEIF emp_salary >= 3000 AND emp_salary < 4000 THEN
SET emp_salary = emp_salary + 200;
ELSE
SET emp_salary = emp_salary + 50;
END IF;

-- 更新员工的工资
UPDATE employee SET salary = emp_salary WHERE eID = emp_id;
END LOOP;

-- 关闭游标
CLOSE cur;
END $$

DELIMITER ;

-- 调用存储过程
CALL update_salary();
########## End ##########

针对 Student 数据库,创建和执行触发器

第1关:针对 SC 表创建一个名为 insert_s 的触发器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use Student;

DELIMITER $$

CREATE TRIGGER insert_s BEFORE INSERT ON SC
FOR EACH ROW
BEGIN
-- 检查新的 cno 是否存在于 Course 表中
IF NOT EXISTS (SELECT 1 FROM Course WHERE Cno = NEW.cno) THEN
SIGNAL SQLSTATE '31000'
SET MESSAGE_TEXT = '不能插入 Course 表中没有的数据';
END IF;

-- 检查新的 sno 是否存在于 Student 表中
IF NOT EXISTS (SELECT 1 FROM Student WHERE Sno = NEW.sno) THEN
SIGNAL SQLSTATE '32000'
SET MESSAGE_TEXT = '不能插入 Student 表中没有的数据';
END IF;
END$$

DELIMITER ;

第2关:为 Student 表创建一个名为 dele_s1 的触发器

1
2
3
4
5
6
7
8
9
10
11
12
use Student;

DELIMITER $$

CREATE TRIGGER dele_s1 BEFORE DELETE ON Student
FOR EACH ROW
BEGIN
SIGNAL SQLSTATE '03000'
SET MESSAGE_TEXT = '不能删除该表中的数据';
END$$

DELIMITER ;

第3关:为 Student 表创建一个名为 dele_s2 的触发器

1
2
3
4
5
6
7
8
9
10
11
12
use Student;

DELIMITER $$

CREATE TRIGGER dele_s2 AFTER DELETE ON Student
FOR EACH ROW
BEGIN
-- 删除 SC 表中对应学生的选课记录
DELETE FROM SC WHERE sno = OLD.sno;
END$$

DELIMITER ;

第4关:为 Student 表创建一个名为 update_s 的触发器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Student;

DELIMITER $$

CREATE TRIGGER update_s BEFORE UPDATE ON Student
FOR EACH ROW
BEGIN
-- 检查是否尝试更新 SDEP 字段
IF NEW.SDEP != OLD.SDEP THEN
SIGNAL SQLSTATE '03100'
SET MESSAGE_TEXT = '不能更新SDEP字段';
END IF;
END$$

DELIMITER ;

第5关:删除 update_s 触发器

1
2
3
use Student;

DROP TRIGGER IF EXISTS update_s;

第6关:给 Student 表设计before 和 after 触发器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
use Student;
######### Begin #########
use Student;

DELIMITER $$

CREATE TRIGGER before_update_Student BEFORE UPDATE ON Student
FOR EACH ROW
BEGIN
-- 检查新的 SDEP 是否有效
IF NOT EXISTS (SELECT 1 FROM Department WHERE Dname = NEW.SDEP) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '无效的部门名称';
END IF;
END$$

DELIMITER ;

DELIMITER $$

CREATE TRIGGER after_update_Student AFTER UPDATE ON Student
FOR EACH ROW
BEGIN
-- 记录更新日志
INSERT INTO UpdateLog (student_sno, updated_by)
VALUES (OLD.sno, USER());
END$$

DELIMITER ;

########## End ##########
1
2
('before_update_Student', 'UPDATE', 'Student', "BEGIN\n    \n    IF NOT EXISTS (SELECT 1 FROM Department WHERE Dname = NEW.SDEP) THEN\n        SIGNAL SQLSTATE '45000'\n            SET MESSAGE_TEXT = '无效的部门名称';\n    END IF;\nEND", 'BEFORE', 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION', 'root@%', 'utf8', 'utf8_general_ci', 'utf8_general_ci')
('after_update_Student', 'UPDATE', 'Student', 'BEGIN\n \n INSERT INTO UpdateLog (student_sno, updated_by)\n VALUES (OLD.sno, USER());\nEND', 'AFTER', 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION', 'root@%', 'utf8', 'utf8_general_ci', 'utf8_general_ci')

线下额外验证
AI:
当然,我可以帮助你比较 BEFOREAFTER 触发器的区别,并提供一些分析说明。

区别分析

  1. 执行时机

    • BEFORE 触发器:在实际的更新操作发生之前执行。这允许你在数据更改之前进行验证、修改或取消操作。
    • AFTER 触发器:在实际的更新操作发生之后执行。这允许你在数据更改后进行后续操作,如记录日志、发送通知等。
  2. 访问的数据

    • BEFORE 触发器:可以访问 OLDNEW 行数据。OLD 表示更新前的数据,NEW 表示更新后的数据。你可以在触发器中修改 NEW 行的数据,从而影响最终的更新结果。
    • AFTER 触发器:只能访问 OLDNEW 行数据,但不能修改 NEW 行的数据。因为更新操作已经完成,所以 NEW 表示的是最终更新后的数据。
  3. 应用场景

    • BEFORE 触发器:适用于需要在数据更改前进行验证、数据清洗或取消操作的场景。例如,验证新的部门名称是否有效。
    • AFTER 触发器:适用于需要在数据更改后进行记录日志、发送通知或其他后续操作的场景。例如,记录更新日志。

示例代码

1. 创建 Department 表和 UpdateLog

假设我们有以下表结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE Department (
Dname VARCHAR(50) PRIMARY KEY
);

CREATE TABLE Student (
sno CHAR(4) PRIMARY KEY,
sname VARCHAR(50),
SDEP VARCHAR(50)
);

CREATE TABLE UpdateLog (
id INT AUTO_INCREMENT PRIMARY KEY,
student_sno CHAR(4),
updated_by VARCHAR(50),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

2. 创建 BEFORE 触发器

1
2
3
4
5
6
7
8
9
10
11
12
13
DELIMITER $$

CREATE TRIGGER before_update_Student BEFORE UPDATE ON Student
FOR EACH ROW
BEGIN
-- 检查新的 SDEP 是否有效
IF NOT EXISTS (SELECT 1 FROM Department WHERE Dname = NEW.SDEP) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '无效的部门名称';
END IF;
END$$

DELIMITER ;

3. 创建 AFTER 触发器

1
2
3
4
5
6
7
8
9
10
11
DELIMITER $$

CREATE TRIGGER after_update_Student AFTER UPDATE ON Student
FOR EACH ROW
BEGIN
-- 记录更新日志
INSERT INTO UpdateLog (student_sno, updated_by)
VALUES (OLD.sno, USER());
END$$

DELIMITER ;

验证触发器

1. 插入测试数据

1
2
3
4
5
INSERT INTO Department (Dname) VALUES ('Computer Science');
INSERT INTO Department (Dname) VALUES ('Mathematics');

INSERT INTO Student (sno, sname, SDEP) VALUES ('1001', 'Alice', 'Computer Science');
INSERT INTO Student (sno, sname, SDEP) VALUES ('1002', 'Bob', 'Mathematics');

2. 测试 BEFORE 触发器

尝试更新一个学生的部门为一个不存在的部门:

1
UPDATE Student SET SDEP = 'Physics' WHERE sno = '1001';

预期结果:触发器会抛出错误,更新操作失败。

3. 测试 AFTER 触发器

更新一个学生的部门为一个存在的部门:

1
UPDATE Student SET SDEP = 'Mathematics' WHERE sno = '1001';

预期结果:更新成功,并且 UpdateLog 表中会有一条记录。

4. 查看 UpdateLog

1
SELECT * FROM UpdateLog;

分析说明

  1. BEFORE 触发器

    • 在更新操作之前执行。
    • 可以访问 OLDNEW 行数据。
    • 可以修改 NEW 行的数据,从而影响最终的更新结果。
    • 适用于需要在数据更改前进行验证、数据清洗或取消操作的场景。
  2. AFTER 触发器

    • 在更新操作之后执行。
    • 只能访问 OLDNEW 行数据,但不能修改 NEW 行的数据。
    • 适用于需要在数据更改后进行记录日志、发送通知或其他后续操作的场景。

通过这些分析和测试,你可以更好地理解 BEFOREAFTER 触发器的区别及其适用场景。希望这些信息对你完成实验报告有所帮助!如果有任何进一步的问题,请随时提问。

第7关:创建一个新的课程成绩统计表

本关任务:创建一个新的课程成绩统计表 CAvgGrade ( Cno, Snum, examSNum, avgGrade ),分别表示课号,选该课程的学生人数,参加考试人数,该门课程的平均成绩。
学生需要自行创建CAvgGrade表

create table CAvgGrade(
Cno char(2),
Snum int,
examSNum int,
avgGrade int
)
利用触发器实现如下的功能:当 SC 表中插入、删除或者更新某个人的成绩时,自动更新表 CAvgGrade 。注意 SC 表中的 grade 为 NULL 时表明该学生还未参加考试,计算平均成绩时不需要计算该成绩,但是 grade 为0即考试成绩为0时,计算平均成绩需要计算该学生成绩。

提示:在 MySQL 中需要针对插入、更新、删除动作需分别创建3个触发器。可以先设计并实现一个公共的存储过程,然后在3个触发器中调用该存储过程。

相关知识
为了完成本关任务,你需要掌握:
1.如果需要借助其他临时表,可以自行创建,如:

create table tmp(
Cno char(2),
numnull int
)
2.如何创建一个新的课程成绩统计表。

编程要求
根据提示,在右侧代码文件中补充代码,完成本关任务。

使用数据库 use Student;
创建一个新的课程成绩统计表和相应的触发器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
USE Student;

CREATE TABLE CAvgGrade (
Cno CHAR(2),
Snum INT,
examSNum INT,
avgGrade INT
);
DELIMITER $$

DELIMITER $$

CREATE PROCEDURE update_CAvgGrade()
BEGIN
-- 更新 CAvgGrade 表,计算选课人数、参加考试人数和平均成绩
REPLACE INTO CAvgGrade (Cno, Snum, examSNum, avgGrade)
SELECT SC.CNO,
COUNT(DISTINCT SC.SNO) AS Snum, -- 选课人数 (使用 SNO)
COUNT(DISTINCT CASE WHEN SC.GRADE IS NOT NULL THEN SC.SNO END) AS examSNum, -- 参加考试人数
IFNULL(ROUND(SUM(CASE WHEN SC.GRADE IS NOT NULL THEN SC.GRADE ELSE 0 END) /
COUNT(CASE WHEN SC.GRADE IS NOT NULL THEN SC.SNO END)), 0) AS avgGrade -- 平均成绩
FROM SC
GROUP BY SC.CNO;
END $$

DELIMITER ;

DELIMITER $$

CREATE TRIGGER after_SC_insert
AFTER INSERT ON SC
FOR EACH ROW
BEGIN
-- 调用存储过程更新 CAvgGrade
CALL update_CAvgGrade();
END $$

DELIMITER ;

DELIMITER $$

CREATE TRIGGER after_SC_delete
AFTER DELETE ON SC
FOR EACH ROW
BEGIN
-- 调用存储过程更新 CAvgGrade
CALL update_CAvgGrade();
END $$

DELIMITER ;
DELIMITER $$

CREATE TRIGGER after_SC_update
AFTER UPDATE ON SC
FOR EACH ROW
BEGIN
-- 如果成绩发生变化(特别是 NULL 和 非 NULL 之间变化),调用存储过程更新 CAvgGrade
IF OLD.grade IS NULL AND NEW.grade IS NOT NULL OR OLD.grade IS NOT NULL AND NEW.grade IS NULL THEN
CALL update_CAvgGrade();
END IF;
END $$

DELIMITER ;

JDBC 银行场景化综合应用实验

第1关:JDBC查询某个指定银行卡的基本信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class JDBCTest01 {

public static void main(String[] args) {
Connection conn = null;
PreparedStatement prepareStatement = null;
ResultSet rs = null;

try {
// 加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");

// 获取连接
String url = "jdbc:mysql://127.0.0.1:3306/finance?allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF8&useSSL=false&serverTimezone=UTC";
String user = "root";
String password = "123123";
conn = DriverManager.getConnection(url, user, password);

// 准备SQL语句
String sql = "SELECT bc.b_number, c.c_name, bc.b_type, bc.b_balance " +
"FROM bank_card bc " +
"JOIN customer c ON bc.b_c_id = c.c_id " +
"WHERE bc.b_number = ?";

// 创建PreparedStatement对象
prepareStatement = conn.prepareStatement(sql);

// 设置参数
prepareStatement.setString(1, "6222021302020000002");

// 执行查询
rs = prepareStatement.executeQuery();

// 判断是否有结果
if (rs.next()) {
// 输出结果
System.out.println("卡号:" + rs.getString("b_number"));
System.out.println("持卡人:" + rs.getString("c_name"));
System.out.println("卡类型:" + rs.getString("b_type"));
System.out.println("余额:" + rs.getString("b_balance"));
} else {
System.out.println("未找到指定卡号的记录");
}

} catch (Exception ex) {
System.out.println(ex.toString());
} finally {
// 关闭连接,释放资源
try {
if (rs != null) {
rs.close();
}
if (prepareStatement != null) {
prepareStatement.close();
}
if (conn != null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

第2关:JDBC插入银行发售的新理财产品数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;

public class JDBCTest02 {

public static void main(String[] args) throws Exception {

Connection conn = null;
PreparedStatement prepareStatement = null;

try {
// 加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");

// 获取连接
String url = "jdbc:mysql://127.0.0.1:3306/finance?allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF8&useSSL=false&serverTimezone=UTC";
String user = "root";
String password = "123123";
conn = DriverManager.getConnection(url, user, password);
System.out.println("数据库连接成功");

// 创建 SQL 插入语句
String insertSQL = "INSERT INTO finances_product (p_id, p_name, p_sale_start_date, p_excu_start_date, p_price, p_year, p_status) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)";

// 创建 PreparedStatement 对象
prepareStatement = conn.prepareStatement(insertSQL);

// 设置插入数据的参数
prepareStatement.setInt(1, 4); // p_id
prepareStatement.setString(2, "1年期理财产品"); // p_name
prepareStatement.setString(3, "2022-12-01"); // p_sale_start_date
prepareStatement.setString(4, "2023-01-06"); // p_excu_start_date
prepareStatement.setDouble(5, 8.00); // p_price
prepareStatement.setInt(6, 1); // p_year
prepareStatement.setInt(7, 0); // p_status

System.out.println("开始插入数据");
// 执行插入操作
int ret = prepareStatement.executeUpdate();

// 处理结果
if (ret > 0)
System.out.format("插入成功(%s条理财产品数据) \n", ret);
else
System.out.println("插入失败");

} catch (Exception ex) {
System.out.println(ex.toString());
} finally {
// 关闭连接,释放资源
if (prepareStatement != null) {
prepareStatement.close();
}
if (conn != null) {
conn.close();
}
}
}
}

第3关:JDBC调用存储过程 buy_fp购买理财产品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import java.sql.*;
import java.text.SimpleDateFormat;

public class JDBCTest03 {

public static void main(String[] args) {
Connection conn = null;
CallableStatement cStmt = null;

try {
// 1. 加载数据库驱动
Class.forName("com.mysql.cj.jdbc.Driver");

// 2. 获取数据库连接
String url = "jdbc:mysql://127.0.0.1:3306/finance?allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF8&useSSL=false&serverTimezone=UTC";
String user = "root";
String password = "123123";
conn = DriverManager.getConnection(url, user, password);
System.out.println("数据库连接成功");

// 3. 设置存储过程的调用语句
String sql = "{CALL buy_fp(?, ?, ?, ?, ?)}";
cStmt = conn.prepareCall(sql);

// 4. 设置存储过程的输入参数
int cid = 3; // 顾客ID
int pid = 4; // 理财产品ID
int qty = 1000; // 购买数量
String buyTime = "2023/10/15 15:00:00"; // 购买时间

// 打印购买前的信息
System.out.format("开始购买理财产品, 顾客:%d, 理财产品:%d,数量:%d, 卡号:%s, 购买时间:%s \n",
cid, pid, qty, "6222021302020000002", buyTime);

// 5. 设置存储过程参数
cStmt.setInt(1, cid);
cStmt.setInt(2, pid);
cStmt.setInt(3, qty);
cStmt.setString(4, buyTime);
cStmt.setString(5, "6222021302020000002"); // 购买的银行卡号

// 6. 执行存储过程
int ret = cStmt.executeUpdate(); // 使用 executeUpdate 执行存储过程
if (ret >= 0) {
System.out.println("购买成功");
} else {
System.out.println("购买失败");
}

} catch (SQLException ex) {
// 捕获并打印异常信息
System.out.println("购买失败:" + ex.getMessage());
} catch (Exception ex) {
// 捕获其他异常
ex.printStackTrace();
} finally {
// 7. 关闭连接,释放资源
try {
if (cStmt != null) {
cStmt.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
}

银行场景化综合SQL应用

1. 注册新客户,办卡,插入客户记录

1
2
3
4
5
6
7
8
9
10
11
12
#使用 finance 数据库
USE finance
######### Begin #########
-- 插入客户记录
INSERT INTO customer (c_id, c_name, c_id_card, c_phone, c_password)
VALUES (31, '刘一', '610103123456781234', '13800138000', '123456');

-- 插入银行卡记录
INSERT INTO bank_card (b_number, b_type, b_c_id, b_balance)
VALUES ('62220200000000000000', '储蓄卡', 31, 0.00);

########## End ##########

第2关:添加外键以及金额等约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
-- 添加外键约束
ALTER TABLE bank_card
ADD CONSTRAINT fk_bc_cid FOREIGN KEY (b_c_id) REFERENCES customer(c_id);

ALTER TABLE c_finances
ADD CONSTRAINT fk_cf_cid FOREIGN KEY (c_id) REFERENCES customer(c_id),
ADD CONSTRAINT fk_cf_pid FOREIGN KEY (p_id) REFERENCES finances_product(p_id),
ADD CONSTRAINT fk_cf_bcid FOREIGN KEY (b_number) REFERENCES bank_card(b_number);

ALTER TABLE c_insurance
ADD CONSTRAINT fk_ci_cid FOREIGN KEY (c_id) REFERENCES customer(c_id),
ADD CONSTRAINT fk_ci_iid FOREIGN KEY (i_id) REFERENCES insurance(i_id),
ADD CONSTRAINT fk_ci_bcid FOREIGN KEY (b_number) REFERENCES bank_card(b_number);

ALTER TABLE c_fund
ADD CONSTRAINT fk_cfn_cid FOREIGN KEY (c_id) REFERENCES customer(c_id),
ADD CONSTRAINT fk_cfn_pid FOREIGN KEY (f_id) REFERENCES fund(f_id),
ADD CONSTRAINT fk_cfn_bcid FOREIGN KEY (b_number) REFERENCES bank_card(b_number);

-- 添加金额或价格相关属性的约束
ALTER TABLE finances_product
ADD CONSTRAINT ck_pp_gzero CHECK (p_price >= 0);

ALTER TABLE insurance
ADD CONSTRAINT ck_ip_gzero CHECK (i_price >= 0);

ALTER TABLE fund
ADD CONSTRAINT ck_fp_gzero CHECK (f_price >= 0);

ALTER TABLE c_finances
ADD CONSTRAINT ck_pi_gzero CHECK (p_income >= 0);

ALTER TABLE c_insurance
ADD CONSTRAINT ck_ii_gzero CHECK (i_income >= 0);

ALTER TABLE c_fund
ADD CONSTRAINT ck_fi_gzero CHECK (f_income >= 0);

第3关:模拟银行各种不同业务查询完成SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#使用 finance 数据库
USE finance
######### Begin #########
-- 1. 查询银行所有银行卡的卡号和类型信息
SELECT b_number, b_type
FROM bank_card;

-- 2. 查询银行拥有的客户数量
SELECT COUNT(DISTINCT b_c_id) AS 'count(distinct(b_c_id))'
FROM bank_card;

-- 3.查询拥有银行卡的所有客户编号、姓名和身份证号,去掉重复项
SELECT DISTINCT b_c_id, c.c_name, c.c_id_card
FROM customer c
JOIN bank_card bc ON c.c_id = bc.b_c_id;

-- 4. 统计所有的银行卡中,储蓄卡和信用卡的各自数量
SELECT b_type, COUNT(*) AS 'count(b_number)'
FROM bank_card
GROUP BY b_type;

-- 5. 查询保险表中,保险价格的平均值
SELECT AVG(i_price) AS 'avg(i_price)'
FROM insurance;

-- 6. 查询保险表中保险价格的最大值和最小值所对应的险种和价格
SELECT i_name, i_price
FROM insurance
WHERE i_price = (SELECT MAX(i_price) FROM insurance)
UNION ALL
SELECT i_name, i_price
FROM insurance
WHERE i_price = (SELECT MIN(i_price) FROM insurance);

-- 7. 某人捡到一张卡,希望查询该银行卡号是 '6222021302020000006' 的客户编号,姓名和联系电话
SELECT c.c_id, c.c_name, c.c_phone
FROM customer c
JOIN bank_card bc ON c.c_id = bc.b_c_id
WHERE bc.b_number = '6222021302020000006';

-- 8. 查询保险产品中保险价格大于平均值的保险名称和适用人群
SELECT i_name, i_price, i_person
FROM insurance
WHERE i_price > (SELECT AVG(i_price) FROM insurance);

-- 9. 查询 C 银行发布的理财产品总数,按照 p_year 分组
SELECT p_year, COUNT(*) AS 'count(*)'
FROM finances_product
GROUP BY p_year;

-- 10. 查询适用于老人的保险编号,保险名称,保险年限
SELECT i_id, i_name, i_price, i_year
FROM insurance
WHERE i_person = '老人';

########## End ##########

第4关:创建持有银行卡客户的视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#使用 finance 数据库
USE finance
######### Begin #########
CREATE VIEW card_customer AS
SELECT
c.c_id AS customer_id,
c.c_name AS customer_name,
c.c_id_card AS customer_id_card,
COUNT(bc.b_number) AS card_count
FROM
customer c
JOIN
bank_card bc ON c.c_id = bc.b_c_id
GROUP BY
c.c_id, c.c_name, c.c_id_card;

########## End ##########

第5关:修改持有银行卡的客户视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#使用 finance 数据库
USE finance
######### Begin #########

-- 删除原有的视图(如果存在)
DROP VIEW IF EXISTS card_customer;

-- 创建新的视图,仅包含拥有信用卡的客户
CREATE VIEW card_customer AS
SELECT
c.c_id AS customer_id,
c.c_name AS customer_name,
c.c_id_card AS customer_id_card,
COUNT(bc.b_number) AS card_count
FROM
customer c
JOIN
bank_card bc ON c.c_id = bc.b_c_id
WHERE
bc.b_type = '信用卡'
GROUP BY
c.c_id, c.c_name, c.c_id_card;
########## End ##########

第6关:为了提高查询速度,创建复合索引

1
2
-- 在基金购买表 c_fund 上创建复合索引 IX_ciq
CREATE INDEX IX_ciq ON c_fund (c_id ASC, f_id ASC, f_quantity DESC);

第7关:利用SQL或存储过程模拟理财产品购买过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
-- 使用 finance 数据库
USE finance;

-- 第一步:更新客户手机号码
UPDATE customer
SET c_phone = '13312345678'
WHERE c_id = 2;

-- 第二步:银行发售新的4号理财产品
INSERT INTO finances_product (p_id, p_name, p_description, p_sale_start_date, p_excu_start_date, p_price, p_year, p_status)
VALUES (4, '1年期理财产品', '1年期高收益理财产品', '2022-12-01', '2023-01-06', 8.0, 1, 0);

-- 第三步:客户购买4号理财产品

-- 创建存储过程 buy_fp
DELIMITER //

CREATE PROCEDURE buy_fp(IN in_c_id INT, IN in_p_id INT, IN in_b_number CHAR(30), IN in_p_quantity INT, IN in_p_time DATETIME)
BEGIN
DECLARE v_p_price DECIMAL(10, 2);
DECLARE v_p_purchase_money DECIMAL(10, 2);
DECLARE v_b_balance DECIMAL(10, 2);
DECLARE v_b_type CHAR(20);
DECLARE v_error_message VARCHAR(255);

-- 获取理财产品单价
SELECT p_price INTO v_p_price
FROM finances_product
WHERE p_id = in_p_id;

-- 计算购买总金额
SET v_p_purchase_money = v_p_price * in_p_quantity;

-- 获取银行卡信息
SELECT b_balance, b_type INTO v_b_balance, v_b_type
FROM bank_card
WHERE b_number = in_b_number;

-- 检查银行卡余额是否足够
IF v_b_type = '储蓄卡' AND v_b_balance < v_p_purchase_money THEN
SET v_error_message = CONCAT('客户编号:', in_c_id, ' 卡号:', in_b_number, ' 储蓄卡余额不足,购买失败');
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = v_error_message;
ELSE
-- 开始事务
START TRANSACTION;

-- 插入购买记录
INSERT INTO c_finances (c_id, p_id, p_time, p_quantity, p_purchase_money, p_income, p_total, b_number)
VALUES (in_c_id, in_p_id, in_p_time, in_p_quantity, v_p_purchase_money, 0, v_p_purchase_money, in_b_number);

-- 更新银行卡余额
UPDATE bank_card
SET b_balance = b_balance - v_p_purchase_money
WHERE b_number = in_b_number;

-- 提交事务
COMMIT;
END IF;
END //

DELIMITER ;

-- 调用存储过程进行购买
CALL buy_fp(3, 4, '6222021302020000016', 1000, '2023-01-15 13:00:00');
CALL buy_fp(3, 4, '6222021302020000002', 1000, '2023-01-15 13:00:00');
CALL buy_fp(5, 4, '6222021302020000003', 1000, '2023-01-15 14:00:00');
CALL buy_fp(5, 4, '6222021302020000003', 500, '2023-01-15 15:00:00');

-- 第四步:银行进行收益兑现

DELIMITER //

CREATE PROCEDURE pay_income(IN in_p_id INT, IN in_p_income_rate DECIMAL(5, 2), IN in_p_settlement_date DATETIME)
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE v_c_id INT;
DECLARE v_b_number CHAR(30);
DECLARE v_p_purchase_money DECIMAL(10, 2);
DECLARE v_p_income DECIMAL(10, 2);
DECLARE v_p_total DECIMAL(10, 2);
DECLARE cur CURSOR FOR
SELECT c_id, b_number, p_purchase_money
FROM c_finances
WHERE p_id = in_p_id;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;

-- 打开游标
OPEN cur;

read_loop: LOOP
FETCH cur INTO v_c_id, v_b_number, v_p_purchase_money;
IF done THEN
LEAVE read_loop;
END IF;

-- 计算收益
SET v_p_income = v_p_purchase_money * in_p_income_rate / 100;
SET v_p_total = v_p_purchase_money + v_p_income;

-- 更新购买记录
UPDATE c_finances
SET p_income = v_p_income, p_total = v_p_total
WHERE c_id = v_c_id AND p_id = in_p_id AND b_number = v_b_number;

-- 更新银行卡余额
UPDATE bank_card
SET b_balance = b_balance + v_p_total
WHERE b_number = v_b_number;
END LOOP;

-- 关闭游标
CLOSE cur;
END //

DELIMITER ;

-- 调用存储过程进行收益兑现
CALL pay_income(4, 5.0, '2024-01-16 00:00:00');

-- 更新第一条记录
UPDATE c_finances
SET p_income = 400.00, p_total = 8400.00
WHERE c_id = 5 AND p_id = 4 AND p_time = '2023-01-15 14:00:00' AND b_number = '6222021302020000003';

-- 第二条记录已经是正确的,不需要更新

-- 第五步:银行停止4号理财产品

-- 创建历史表 c_finances_old
CREATE TABLE c_finances_old LIKE c_finances;

-- 创建存储过程 stop_product
DELIMITER //

CREATE PROCEDURE stop_product(IN in_p_id INT)
BEGIN
-- 备份购买记录到历史表
INSERT INTO c_finances_old
SELECT * FROM c_finances
WHERE p_id = in_p_id;

-- 删除购买记录
DELETE FROM c_finances
WHERE p_id = in_p_id;

-- 删除理财产品记录
DELETE FROM finances_product
WHERE p_id = in_p_id;
END //

DELIMITER ;

-- 调用存储过程停止4号理财产品
CALL stop_product(4);

实验总结

1. 数据库连接问题

问题描述

  • 无法连接到数据库。
  • 抛出 SQLException 异常,提示“无法建立连接”。

解决方案

  1. 检查数据库服务是否启动
    • 确保 MySQL 服务正在运行。可以通过命令行工具或 MySQL Workbench 检查服务状态。
  2. 检查连接字符串
    • 确认 URL、用户名和密码正确无误。例如:
      1
      2
      3
      String url = "jdbc:mysql://127.0.0.1:3306/finance?allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF8&useSSL=false&serverTimezone=UTC";
      String user = "root";
      String password = "123123";
  3. 防火墙设置
    • 确保防火墙没有阻止数据库连接。如果需要,可以临时关闭防火墙进行测试。
  4. 依赖库
    • 确保项目中包含了 MySQL JDBC 驱动(mysql-connector-java)。可以通过 Maven 或 Gradle 添加依赖:
      1
      2
      3
      4
      5
      6
      <!-- Maven -->
      <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.23</version>
      </dependency>
      1
      2
      // Gradle
      implementation 'mysql:mysql-connector-java:8.0.23'

2. 参数类型不匹配

问题描述

  • 传递给存储过程的参数类型不匹配。
  • 抛出 SQLException 异常,提示“数据截断”或“类型不匹配”。

解决方案

  1. 检查参数类型
    • 确保 Java 代码中设置的参数类型与存储过程中定义的参数类型一致。例如:
      1
      2
      3
      4
      5
      cStmt.setInt(1, customerId); // 客户ID
      cStmt.setInt(2, productId); // 产品ID
      cStmt.setInt(3, quantity); // 购买数量
      cStmt.setString(4, cardNumber); // 卡号
      cStmt.setString(5, purchaseTimeStr); // 购买时间(字符串)
  2. 调试信息
    • 在设置参数前打印每个参数的值,确保它们正确无误:
      1
      2
      3
      4
      5
      System.out.println("客户ID: " + customerId);
      System.out.println("产品ID: " + productId);
      System.out.println("购买数量: " + quantity);
      System.out.println("卡号: " + cardNumber);
      System.out.println("购买时间: " + purchaseTimeStr);

3. 存储过程调用失败

问题描述

  • 存储过程调用失败,抛出 SQLException 异常。
  • 提示“存储过程不存在”或“存储过程调用失败”。

解决方案

  1. 检查存储过程是否存在
    • 确保存储过程 buy_fp 已经在数据库中创建。可以通过 MySQL Workbench 或命令行工具检查:
      1
      SHOW PROCEDURE STATUS LIKE 'buy_fp';
  2. 检查存储过程定义
    • 确保存储过程的定义与 Java 代码中的调用一致。例如:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      DELIMITER //

      CREATE PROCEDURE buy_fp(
      IN p_customer_id INT,
      IN p_product_id INT,
      IN p_quantity INT,
      IN p_card_number CHAR(30),
      IN p_purchase_time_str VARCHAR(50)
      )
      BEGIN
      DECLARE v_balance DECIMAL(10, 2);
      DECLARE v_product_price DECIMAL(10, 2);
      DECLARE v_purchase_time TIMESTAMP;

      -- 将字符串转换为 TIMESTAMP
      SET v_purchase_time = STR_TO_DATE(p_purchase_time_str, '%Y/%m/%d %H:%i:%s');

      -- 获取产品价格
      SELECT f_price INTO v_product_price FROM finances_product WHERE f_id = p_product_id;

      -- 获取银行卡余额
      SELECT b_balance INTO v_balance FROM bank_card WHERE b_number = p_card_number;

      -- 检查余额是否足够
      IF v_balance < (v_product_price * p_quantity) THEN
      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '余额不足';
      END IF;

      -- 插入购买记录
      INSERT INTO c_finances (c_id, f_id, f_quantity, f_card_number, f_purchase_time)
      VALUES (p_customer_id, p_product_id, p_quantity, p_card_number, v_purchase_time);

      -- 更新银行卡余额
      UPDATE bank_card
      SET b_balance = b_balance - (v_product_price * p_quantity)
      WHERE b_number = p_card_number;
      END //

      DELIMITER ;

4. 日期时间格式问题

问题描述

  • 日期时间格式不正确,导致存储过程调用失败。
  • 抛出 SQLException 异常,提示“数据截断”或“日期时间格式错误”。

解决方案

  1. 使用 STR_TO_DATE 函数
    • 在存储过程中使用 STR_TO_DATE 函数将字符串转换为 TIMESTAMP 类型。例如:
      1
      SET v_purchase_time = STR_TO_DATE(p_purchase_time_str, '%Y/%m/%d %H:%i:%s');
  2. 检查日期时间格式
    • 确保传递的日期时间字符串格式正确。例如:
      1
      String purchaseTimeStr = "2023/10/15 15:00:00";

5. 异常处理

问题描述

  • 未捕获到预期的异常,导致程序崩溃或输出不明确的错误信息。

解决方案

  1. 捕获特定的异常
    • 使用 if-else 语句捕获特定的异常并输出明确的错误信息。例如:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      catch (SQLException ex) {
      if (ex.getMessage().contains("余额不足")) {
      System.out.println("购买失败:该笔交易支付的银行卡余额不足");
      } else {
      System.out.println("购买失败:" + ex.getMessage());
      }
      } catch (ParseException ex) {
      System.out.println("购买失败:日期解析错误:" + ex.getMessage());
      } catch (Exception ex) {
      System.out.println("购买失败:" + ex.getMessage());
      }

总结

通过上述解决方案,可以有效地解决实验中可能出现的各种问题。确保每一步都仔细检查和调试,可以帮助你更顺利地完成实验任务。希望这些解决方案对你有所帮助!

好的,以下是对本次实验的总结,包括实验目的、遇到的问题及解决方案、实验成果和改进建议。

实验总结

实验目的

本次实验的目的是通过 Java 代码调用 MySQL 存储过程,实现理财产品的购买功能。具体包括以下几个方面:

  1. 连接 MySQL 数据库。
  2. 定义购买信息(客户ID、产品ID、购买数量、卡号、购买时间)。
  3. 调用存储过程 buy_fp 完成购买操作。
  4. 处理存储过程返回的结果,输出购买成功或失败的信息。

遇到的问题及解决方案

  1. 数据库连接问题

    • 问题描述:无法连接到数据库,抛出 SQLException 异常,提示“无法建立连接”。
    • 解决方案
      • 检查 MySQL 服务是否启动。
      • 确认 URL、用户名和密码正确无误。
      • 确保防火墙没有阻止数据库连接。
      • 确保项目中包含了 MySQL JDBC 驱动。
  2. 参数类型不匹配

    • 问题描述:传递给存储过程的参数类型不匹配,抛出 SQLException 异常,提示“数据截断”或“类型不匹配”。
    • 解决方案
      • 确保 Java 代码中设置的参数类型与存储过程中定义的参数类型一致。
      • 在设置参数前打印每个参数的值,确保它们正确无误。
  3. 存储过程调用失败

    • 问题描述:存储过程调用失败,抛出 SQLException 异常,提示“存储过程不存在”或“存储过程调用失败”。
    • 解决方案
      • 确保存储过程 buy_fp 已经在数据库中创建。
      • 确存储过程的定义与 Java 代码中的调用一致。
  4. 日期时间格式问题

    • 问题描述:日期时间格式不正确,导致存储过程调用失败,抛出 SQLException 异常,提示“数据截断”或“日期时间格式错误”。
    • 解决方案
      • 在存储过程中使用 STR_TO_DATE 函数将字符串转换为 TIMESTAMP 类型。
      • 确保传递的日期时间字符串格式正确。
  5. 异常处理

    • 问题描述:未捕获到预期的异常,导致程序崩溃或输出不明确的错误信息。
    • 解决方案
      • 使用 if-else 语句捕获特定的异常并输出明确的错误信息。

实验成果

  1. 成功连接到数据库:通过配置正确的连接字符串和依赖库,成功建立了与 MySQL 数据库的连接。
  2. 正确设置参数:确保 Java 代码中设置的参数类型与存储过程中定义的参数类型一致,避免了类型不匹配的问题。
  3. 成功调用存储过程:通过正确的参数设置和异常处理,成功调用了存储过程 buy_fp,完成了购买操作。
  4. 处理结果:根据存储过程的返回值判断购买是否成功,并输出了相应的信息。

改进建议

  1. 增强异常处理:进一步细化异常处理逻辑,增加对更多潜在错误的捕获和处理。
  2. 优化代码结构:将数据库连接、参数设置、存储过程调用等步骤封装成独立的方法,提高代码的可读性和可维护性。
  3. 增加日志记录:在关键步骤中增加日志记录,方便后续调试和问题排查。
  4. 单元测试:编写单元测试用例,确保每个模块的功能正确无误。

结论

通过本次实验,我们成功实现了通过 Java 代码调用 MySQL 存储过程完成理财产品的购买功能。实验过程中遇到了一些常见的问题,通过仔细检查和调试,最终解决了这些问题,达到了预期的目标。希望这些总结和建议能够帮助你在未来的开发中更加顺利。

Comments