qqzone项目
1.熟悉QQZone业务需求
1. 用户登录
- 登录成功,显示主界面。左侧显示好友列表;上端显示欢迎词。如果不是自己的空间,显示超链接:返回自己的空间;下端显示日志列表
- 查看日志详情:
- 日志本身的信息(作者头像、昵称、日志标题、日志内容、日志的日期)
- 回复列表(回复者的头像、昵称、回复内容、回复日期)
- 主人回复信息
4. 删除日志
5. 删除特定回复
6. 删除特定主人回复
7. 添加日志、添加回复、添加主人回复
- 点击左侧好友链接,进入好友的空间
2.数据库设计
-
抽取实体 :
用户登录信息、用户详情信息 、 日志 、 回贴 、 主人回复
-
分析其中的属性:
-
用户登录信息:账号、密码、头像、昵称
-
用户详情信息:真实姓名、星座、血型、邮箱、手机号…
-
日志:标题、内容、日期、作者
-
回复:内容、日期、作者、日志
-
主人回复:内容、日期、作者、回复
-
-
分析实体之间的关系 (一对一 ,一对多)
-
用户登录信息 : 用户详情信息 1:1 PK
(Primary Key)
-
用户 : 日志 1:N
-
日志 : 回复 1:N
-
回复 : 主人回复 1:1 UK
-
用户 : 好友 M : N (一个人多个好友)
-
3.数据库的范式:
一般情况下不要让主键与业务有关连
-
第一范式:列不可再分
最核心,最重要的范式,所有表的设计==都需要满足==。
- 必须有主键,并且每一个字段都是原子性不可再分
-
第二范式:一张表只表达一层含义(==只描述一件事情==)
- 建立在第一范式的基础之上,
要求所有非主键字段必须完全依赖主键,不要产生部分依赖。
- 建立在第一范式的基础之上,
-
第三范式:表中的每一列和主键都是==直接依赖关系==,而不是间接依赖
- 第三范式建立在第二范式的基础之上
要求所有非主键字典必须直接依赖主键,不要产生传递依赖。
- 第三范式建立在第二范式的基础之上
数据库设计的范式和数据库的查询性能很多时候是相悖的,我们需要根据实际的业务情况做一个选择:
- 查询==频次不高==的情况下,我们更倾向于提高数据库的设计范式,从而提高存储效率
- 查询==频次较高==的情形,我们更倾向于牺牲数据库的规范度,降低数据库设计的范式,允许特定的冗余,从而提高查询的性能
4.QQZone登录功能实现出现的四个错误:
1)URL没修改,用的还是fruitdb
2)
3)rsmd.getColumnName() 和 rsmd.getColumnLabel()
4)Can not set com.atguigu.qqzone.pojo.UserBasic field com.atguigu.qqzone.pojo.Topic.author to java.lang.Integer
-
left.html页面没有样式,同时数据也不展示,原因是:我们是直接去请求的静态页面资源,那么并没有执行
super.processTemplate()
,也就是thymeleaf没有起作用
(之前的表单也是这个原因)
解决方法:-
新增PageController,添加page方法:
1
2
3public String page(String page){
return page ; // frames/left
}目的是执行
super.processTemplate()
方法,让thymeleaf生效
-
理解
-
首先进入
http://localhost:8080/pro22/page.do?operate=page&page=login
, 输入账号密码后 ,系统会根据输入的账号密码去数据库查询 -
最基本的
UserBasicDAOImpl
(pojo层Java中的POJO)只是根据账户(UserBasic) 来获取 fid List ,实际上算不上是FriendList1
2
3
4
5
public List<UserBasic> getUserBasicList(UserBasic userBasic) {
String sql = "SELECT fid as 'id' FROM t_friend WHERE uid = ?";
return super.executeQuery(sql,userBasic.getId());
} -
然后会进入service层,
UserBasicServiceImpl
中的getFriendList
方法才是真正的获取FriendList1
2
3
4
5
6
7
8
9
10
11public List<UserBasic> getFriendList(UserBasic userBasic) {
List<UserBasic> userBasicList = userBasicDAO.getUserBasicList(userBasic);
List<UserBasic> friendList = new ArrayList<>(userBasicList.size());
for (int i = 0; i < userBasicList.size(); i++) {
UserBasic friend = userBasicList.get(i);
friend = userBasicDAO.getUserBasicById(friend.getId());
friendList.add(friend);
}
return friendList;
}关键在于
userBasicDAO
的==getUserBasicById==方法
1 |
|
-
此时已经完成了账户好友的加载, 然后进行topicList 的获取
-
依然是
TopicServiceImpl
内的getTopicList
1
2
3
4
public List<Topic> getTopicList(UserBasic userBasic) {
return topicDAO.getTopicList(userBasic);
} -
然后是
TopicDAOImpl
内的getTopicList
,注意这个类是实现了TopicDAO 接口1
2
3
4
public List<Topic> getTopicList(UserBasic userBasic) {
return super.executeQuery("select * from t_topic where author = ? " , userBasic.getId());
} -
接下来来看
super.executeQuery
的实现原理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//执行查询,返回List executeQuery:执行查询
protected List<T> executeQuery(String sql , Object... params){
List<T> list = new ArrayList<>();
conn = getConn() ;
try{
psmt = conn.prepareStatement(sql);
setParams(psmt,params);
rs = psmt.executeQuery();
//通过rs可以获取结果集的元数据
//元数据:描述结果集数据的数据 , 简单讲,就是这个结果集有哪些列,什么类型等等
ResultSetMetaData rsmd = rs.getMetaData();
//获取结果集的列数
int columnCount = rsmd.getColumnCount();
//6.解析rs
while(rs.next()){
T entity = (T)entityClass.newInstance();
for(int i = 0 ; i<columnCount;i++){
String columnName = rsmd.getColumnLabel(i+1); //fid fname price
Object columnValue = rs.getObject(i+1); //33 苹果 5
setValue(entity,columnName,columnValue);
}
list.add(entity);
}
}catch (Exception e){
e.printStackTrace();
throw new DAOException("BaseDAO executeQuery出错了");
}
return list ;
}其中
setValue()
方法需要注意通过反射技术给obj对象的property属性赋propertyValue值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25private void setValue(Object obj , String property , Object propertyValue)
throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
Class clazz = obj.getClass();
//获取property这个字符串对应的属性名 , 比如 "fid" 去找 obj对象中的 fid 属性
Field field = clazz.getDeclaredField(property);
if(field!=null){
//获取当前字段的类型名称
String typeName = field.getType().getName();
//判断如果是自定义类型,则需要调用这个自定义类的带一个参数的构造方法,创建出这个自定义的实例对象,然后将实例对象赋值给这个属性
if(isMyType(typeName)){
//假设typeName是"com.atguigu.qqzone.pojo.UserBasic"
Class typeNameClass = Class.forName(typeName);
Constructor constructor = typeNameClass.getDeclaredConstructor(java.lang.Integer.class);
propertyValue = constructor.newInstance(propertyValue);
}
field.setAccessible(true);
if(propertyValue instanceof LocalDateTime)
{
propertyValue = Timestamp.valueOf((LocalDateTime) propertyValue);
}
field.set(obj,propertyValue);
}
}
-
bug1
setValue方法在给 topic
: private Date topicDate ;
赋值时 出现异常:
-
Can not set java.util.Date field com.atguigu.qqzone.pojo.Topic.topicDate to java.time.LocalDateTime
-
无法将LocalDateTime类型数据赋值给Date类型变量
原因 :应该属于mysql8.0驱动的bug,具体原因未知
正常情况下从表中查出datetime类型如下
出现bug时类型为LocalDateTime,值为2017-07-15T00:16:40.0,与pojo属性类型不匹配
采用的解决方法是在属性赋值前(setValue
)进行类型检查,如果是LocalDateTime类型就转换成java.util.Date类型
- 正确的代码已经在上面改过了
- 注意 :
propertyValue=(Date)propertyValue;
直接转换会==报错==class java.time.LocalDateTime cannot be cast to class java.util.Date (java.time.LocalDateTime and java.util.Date are in module java.base of loader 'bootstrap')
1 | if (propertyValue.getClass().toString().equals("class java.time.LocalDateTime")) |
==bug==解除,可以正常加载页面
遇到的问题
总结:java的properties文件赋值不要写分号,会一起读取进去
==背景==:在将登录MySQL需要用到的数据封装到jdbc.properties时,
- 通过静态代码块,加载类时自动执行(读取jdbc.properties中的数据)
1 | public static String DRIVER ; |
==异常==:出现加载驱动错误
java.sql.SQLException: No suitable driver found for "jdbc:mysql://localhost:3306/qqzonedb2?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&useSSL=false";
==原因==:java的properties文件不能写分号
-
properties在load()时会将分号一起读取进去
-
因为
jdbc.properties
配置文件中的jdbc.DRIVER
后面带了一个分号,所以通过load获取的Driver也有一个分号(上面的debug图片) -
注意上面的异常最后也是带了一个分号
-
删去分号,问题解决
1、top.html页面显示登录者昵称、判断是否是自己的空间
1)显示登录者昵称: ${session.userBasic.nickName}
2)判断是否是自己的空间 : ${session.userBasic.id!=session.friend.id}
如果不是期望的效果,首先考虑将两者的id都显示出来
2、点击左侧的好友链接,进入好友空间
-
根据id获取指定userBasic信息,查询这个userBasic的topicList,然后覆盖friend对应的value
-
main页面应该展示friend中的topicList,而不是userBasic中的topicList
- 跳转后,在左侧(left)中显示整个index页面
- 问题:在left页面显示整个index布局
- 解决:给超链接添加target属性: target=“_top” 保证在顶层窗口显示整个index页面
-
top.html页面需要修改:
"欢迎进入${session.friend}"
-
top.html页面的返回自己空间的超链接需要修改:
<a th:href="@{|/user.do?operate=friend&id=${session.userBasic.id}|}" target="_top">
-
==_top== 说白了就是覆盖原有的界面,
-
3、日志详情页面实现
- 已知topic的id,需要根据topic的id获取特定topic
- 获取这个topic关联的所有的回复
- 如果某个回复有主人回复,需要查询出来
- 在TopicController中获取指定的topic
- 具体这个topic中关联多少个Reply,由ReplyService内部实现
-
获取到的topic中的author只有id,那么需要在topicService的getTopic方法中封装,在查询topic本身信息时,
同时调用userBasicService中的获取userBasic方法,给author属性赋值
1
2
3
4
5
6
public Topic getTopic(Integer id){
Topic topic = topicDAO.getTopic(id);
topic.setAuthor(userBasicService.getUserBasicById(topic.getAuthor().getId()));
return topic;
}
- 同理,在reply类中也有author,而且这个author也是只有id,那么我们也需要根据id查询得到author,最后设置关联
4、添加回复
5、删除回复
- 如果回复有关联的主人回复,需要先删除主人回复
- 删除回复
Cannot delete or update a parent row: a foreign key constraint fails
(qqzonedb
.t_host_reply
, CONSTRAINTFK_host_reply
FOREIGN KEY (reply
) REFERENCESt_reply
(id
))
我们在删除回复表记录时,发现删除失败,原因是:在主人回复表中仍然有记录引用待删除的回复这条记录
如果需要删除主表数据,需要首先删除子表数据
6、删除日志
- 删除日志,首先需要考虑是否有关联的回复
- 删除回复,首先需要考虑是否有关联的主人回复
- 另外,如果不是自己的空间,则不能删除日志
Can not set
java.util.Date
field com.atguigu.qqzone.pojo.Topic.topicDate
to
java.time.LocalDateTime
增加添加日志功能
—ByMyself
主要增加了
-
addTopic.html
<a th:href="@{|/addTopic.do?operate=addTopic&userBasicId=${session.userBasic.id}|}" class="right8" target="_blank" >发表新日志</a>
- a标签向后端传值
-
addTopic 方法 :
-
TopicController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public String addTopic(Integer userBasicId,HttpSession session){
// 跳转到 addTopic.html 填入内容
UserBasic userBasic = userBasicDAO.getUserBasicById(userBasicId);
session.setAttribute("userBasic",userBasic);
return "frames/addTopic";
}
public String updateTopic(String title, String content,Integer Id,HttpSession session)
{
// 通过 DispacherServlet 接收来自addTopic.html的数据 ,并保存到数据库
UserBasic author=(UserBasic)session.getAttribute("userBasic");
Topic topic=new Topic(content,new Date(),title,Id,author);
topicDAO.addTopic(topic);
return "redirect:user.do?operate=login&loginId="+author.getLoginId()+"&pwd="+author.getPwd();
/* return "index"; 直接返回index 就不会登录,无法显示个人信息(账号, 以及日志等)*/
} -
TopicDAOImpl
1
2
3
4
5
6
7
8
9
public void addTopic(Topic topic) {
executeUpdate("insert into t_reply values(?,?,?,?,?)",
topic.getId(),
topic.getTitle(),
topic.getContent(),
topic.getTopicDate(),
topic.getAuthor().getId()) ;
}
-
-
大概执行顺序
-
在main.html点击发表新日志
<a th:href="@{/addTopic.do}" class="right8" >发表新日志2</a>
-
DispacherServlet接收
/addTopic.do
请求, 从请求中获取operate(此处是 addTopic) ,调用TopicController(通过beanFactory获取)addTopic方法, -
DispacherServle接收addTopic方法的返回值(
String returnStr = method.invoke(controllerBeanObj,parameterValues)
😉,然后通过thymeleaf模板引擎加载前端页面 -
在addTopic.html填入数据后,点击提交,将表单(
<form th:action="@{|/updateTopic.do?operate=updateTopic&userBasicId=${session.userBasic.id}|}">
>)发送到DicpacherServlet,beanFactory从application.xml读取到请求对应的controller(这里就是TopicController),然后找到operate对应的方法(这里就是updateTopic方法),将数据保存到数据库,返回登录页面
1
return "redirect:user.do?operate=login&loginId="+author.getLoginId()+"&pwd="+author.getPwd();
- 这里仍然是DispacherServle接收addTopic方法的返回值,然后thymeleaf模板引擎加载前端页面
-
添加日志操作完成
-
main.html
1 | <a th:href="@{|/addTopic.do?operate=addTopic&userBasicId=${session.userBasic.id}|}" class="right8" target="_blank" >发表新日志</a> |
addTopic.html
1 |
|
遇到的问题:
- addTopic方法的
return "frames/detail";
的frames 没有写s - 执行的顺序应该是main.html 点击添加,发送请求, 然后执行addTopic 跳转到addTopic.html,在addTopic.html修改完成后,提交form表单,发送请求到TopicController的updateTopic,在uodateTopic内将数据保存到数据库
- 第一次写的时候直接在addTopic方法里保存到数据库,但那是明显不可行的
- TopicDAOImpl 的addTopic 方法需要插入的表写错了,导致日期格式不正确
- insert into t_topic 写成了 insert into t_reply