1.熟悉QQZone业务需求

image-20220724195516797

1. 用户登录
  1. 登录成功,显示主界面。左侧显示好友列表;上端显示欢迎词。如果不是自己的空间,显示超链接:返回自己的空间;下端显示日志列表
  1. 查看日志详情:
    • 日志本身的信息(作者头像、昵称、日志标题、日志内容、日志的日期)
    • 回复列表(回复者的头像、昵称、回复内容、回复日期)
    • 主人回复信息
4. 删除日志
5. 删除特定回复
6. 删除特定主人回复
7. 添加日志、添加回复、添加主人回复
  1. 点击左侧好友链接,进入好友的空间
image-20220709131718987

2.数据库设计

  1. 抽取实体 :

    用户登录信息、用户详情信息 、 日志 、 回贴 、 主人回复

  2. 分析其中的属性:

    • 用户登录信息:账号、密码、头像、昵称

    • 用户详情信息:真实姓名、星座、血型、邮箱、手机号…

    • 日志:标题、内容、日期、作者

    • 回复:内容、日期、作者、日志

    • 主人回复:内容、日期、作者、回复

  3. 分析实体之间的关系 (一对一 ,一对多)

    • 用户登录信息 : 用户详情信息 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

  1. left.html页面没有样式,同时数据也不展示,原因是:我们是直接去请求的静态页面资源,那么并没有执行super.processTemplate(),也就是thymeleaf没有起作用
    (之前的表单也是这个原因)
    解决方法:

    • 新增PageController,添加page方法:

      1
      2
      3
      public String page(String page){
      return page ; // frames/left
      }

      目的是执行super.processTemplate()方法,让thymeleaf生效

03.查询好友列表

理解

  1. 首先进入http://localhost:8080/pro22/page.do?operate=page&page=login , 输入账号密码后 ,系统会根据输入的账号密码去数据库查询

  2. 最基本的UserBasicDAOImpl (pojo层Java中的POJO)只是根据账户(UserBasic) 来获取 fid List ,实际上算不上是FriendList

    1
    2
    3
    4
    5
    @Override
    public List<UserBasic> getUserBasicList(UserBasic userBasic) {
    String sql = "SELECT fid as 'id' FROM t_friend WHERE uid = ?";
    return super.executeQuery(sql,userBasic.getId());
    }
  3. 然后会进入service层, UserBasicServiceImpl中的getFriendList 方法才是真正的获取FriendList

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public 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
2
3
4
@Override
public UserBasic getUserBasicById(Integer id) {
return load("select * from t_user_basic where id = ? " , id);
}
  1. 此时已经完成了账户好友的加载, 然后进行topicList 的获取

    1. 依然是TopicServiceImpl内的getTopicList

      1
      2
      3
      4
      @Override
      public List<Topic> getTopicList(UserBasic userBasic) {
      return topicDAO.getTopicList(userBasic);
      }
    2. 然后是TopicDAOImpl内的getTopicList ,注意这个类是实现了TopicDAO 接口

      1
      2
      3
      4
      @Override
      public List<Topic> getTopicList(UserBasic userBasic) {
      return super.executeQuery("select * from t_topic where author = ? " , userBasic.getId());
      }
    3. 接下来来看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
      25
      private 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方法在给 topicprivate 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
2
3
4
5
if (propertyValue.getClass().toString().equals("class java.time.LocalDateTime"))
//实际测试使用 if(propertyValue instanceof LocalDateTime) 也可以
{
propertyValue = Timestamp.valueOf((LocalDateTime) propertyValue);
}

==bug==解除,可以正常加载页面

遇到的问题

总结:java的properties文件赋值不要写分号,会一起读取进去

==背景==:在将登录MySQL需要用到的数据封装到jdbc.properties时,

  • 通过静态代码块,加载类时自动执行(读取jdbc.properties中的数据)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static  String DRIVER ;
public static String URL;
public static String USER ;
public static String PWD ;

static {
InputStream is = ConnUtil.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties properties=new Properties();
try {
properties.load(is);
DRIVER= properties.getProperty("jdbc.DRIVER");
URL= properties.getProperty("jdbc.URL");
USER= properties.getProperty("jdbc.USER");
PWD= properties.getProperty("jdbc.PWD");
} catch (IOException e) {
e.printStackTrace();
}
}

==异常==:出现加载驱动错误

  • 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()时会将分号一起读取进去

    exception

    image-20220710124815097

    • 因为jdbc.properties配置文件中的jdbc.DRIVER后面带了一个分号,所以通过load获取的Driver也有一个分号(上面的debug图片)

    • 注意上面的异常最后也是带了一个分号

删去分号,问题解决


1、top.html页面显示登录者昵称、判断是否是自己的空间

1)显示登录者昵称: ${session.userBasic.nickName}
2)判断是否是自己的空间 : ${session.userBasic.id!=session.friend.id}
如果不是期望的效果,首先考虑将两者的id都显示出来

2、点击左侧的好友链接,进入好友空间

  1. 根据id获取指定userBasic信息,查询这个userBasic的topicList,然后覆盖friend对应的value

  2. main页面应该展示friend中的topicList,而不是userBasic中的topicList

  1. 跳转后,在左侧(left)中显示整个index页面
    • 问题:在left页面显示整个index布局
    • 解决:给超链接添加target属性: target=“_top” 保证在顶层窗口显示整个index页面
  1. top.html页面需要修改: "欢迎进入${session.friend}"

    • top.html页面的返回自己空间的超链接需要修改:
      <a th:href="@{|/user.do?operate=friend&id=${session.userBasic.id}|}" target="_top">

    • ==_top== 说白了就是覆盖原有的界面,

3、日志详情页面实现

  1. 已知topic的id,需要根据topic的id获取特定topic
  2. 获取这个topic关联的所有的回复
  3. 如果某个回复有主人回复,需要查询出来
  • 在TopicController中获取指定的topic
  • 具体这个topic中关联多少个Reply,由ReplyService内部实现
  1. 获取到的topic中的author只有id,那么需要在topicService的getTopic方法中封装,在查询topic本身信息时,

    同时调用userBasicService中的获取userBasic方法,给author属性赋值

    1
    2
    3
    4
    5
    6
    @Override
    public Topic getTopic(Integer id){
    Topic topic = topicDAO.getTopic(id);
    topic.setAuthor(userBasicService.getUserBasicById(topic.getAuthor().getId()));
    return topic;
    }
  1. 同理,在reply类中也有author,而且这个author也是只有id,那么我们也需要根据id查询得到author,最后设置关联

4、添加回复

5、删除回复

  1. 如果回复有关联的主人回复,需要先删除主人回复
  2. 删除回复
    Cannot delete or update a parent row: a foreign key constraint fails
    (qqzonedb.t_host_reply, CONSTRAINT FK_host_reply FOREIGN KEY (reply) REFERENCES t_reply (id))
    我们在删除回复表记录时,发现删除失败,原因是:在主人回复表中仍然有记录引用待删除的回复这条记录
    如果需要删除主表数据,需要首先删除子表数据

6、删除日志

  1. 删除日志,首先需要考虑是否有关联的回复
  2. 删除回复,首先需要考虑是否有关联的主人回复
  3. 另外,如果不是自己的空间,则不能删除日志

Can not set
java.util.Date

​ field com.atguigu.qqzone.pojo.Topic.topicDate
​ to

java.time.LocalDateTime

增加添加日志功能

—ByMyself

主要增加了

  1. addTopic.html

    • <a th:href="@{|/addTopic.do?operate=addTopic&userBasicId=${session.userBasic.id}|}" class="right8" target="_blank" >发表新日志</a>
    • a标签向后端传值
  2. addTopic 方法 :

    • TopicController

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      public 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
      @Override
      public void addTopic(Topic topic) {
      executeUpdate("insert into t_reply values(?,?,?,?,?)",
      topic.getId(),
      topic.getTitle(),
      topic.getContent(),
      topic.getTopicDate(),
      topic.getAuthor().getId()) ;
      }
  3. 大概执行顺序

    1. 在main.html点击发表新日志

      <a th:href="@{/addTopic.do}" class="right8" >发表新日志2</a>

    2. DispacherServlet接收/addTopic.do请求, 从请求中获取operate(此处是 addTopic) ,调用TopicController(通过beanFactory获取)addTopic方法,

    3. DispacherServle接收addTopic方法的返回值(String returnStr = method.invoke(controllerBeanObj,parameterValues)😉,然后通过thymeleaf模板引擎加载前端页面

    4. 在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模板引擎加载前端页面
    5. 添加日志操作完成

main.html

1
<a th:href="@{|/addTopic.do?operate=addTopic&userBasicId=${session.userBasic.id}|}" class="right8" target="_blank" >发表新日志</a>

addTopic.html

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
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<link th:src="@{../css/addTopic.css}">
<script language="JavaScript">
/*此处写入js 代码*/
</script>
<meta charset="UTF-8">
<title>发表新日志</title>
</head>
<!--<div th:text="用户 |${session.userBasic.nickName}| 发表新日志">欢迎进入空间</div>-->

<body>

<form th:action="@{|/updateTopic.do?operate=updateTopic&userBasicId=${session.userBasic.id}|}">
<!-- "@{|/addTopic.do?operate=addTopic&userBasicId=${session.userBasic.id}|}"-->
<!--这一行 表明 要访问TopicController
<input type="hidden" name="operate" value="updateTopic"> ;这一行 表明 要调用 addTopic 方法 -->
<!-- <input type="hidden" name="userBasicId" th:value="${session.userBasic.id}"/>
将这两行 集成到 form 里面 -->

标题<input id="div_add_reply" type="text" name="title" th:value="Test" /> <br/>
日志id<input type="text" name="Id" th:value="12"/> <br/>
主要内容<input type="text" name="content" th:value="主要内容"/> <br/>
<input type="submit" value="添加日志"/> <br/>
<!-- <input type="button" value="删除" th:if="${session.userBasic.id==session.friend.id}" th:onclick="|delTopic(${topic.id})|"/>-->

</form>
</body>
</html>

遇到的问题:

  1. addTopic方法的return "frames/detail";的frames 没有写s
  2. 执行的顺序应该是main.html 点击添加,发送请求, 然后执行addTopic 跳转到addTopic.html,在addTopic.html修改完成后,提交form表单,发送请求到TopicController的updateTopic,在uodateTopic内将数据保存到数据库
    • 第一次写的时候直接在addTopic方法里保存到数据库,但那是明显不可行的
  3. TopicDAOImpl 的addTopic 方法需要插入的表写错了,导致日期格式不正确
    • insert into t_topic 写成了 insert into t_reply