-
用户登录
-
登录成功,显示主界面。左侧显示好友列表;上端显示欢迎词,如果不是自己的空间,显示超链接:返回自己的空间;下端显示日志列表
-
查看日志详情:
- 日志本身的信息(作者头像、昵称、日志标题、日志内容、日志的日期)
- 回复列表(回复者的头像、昵称、回复内容、回复日期)
- 主人回复信息
-
删除日志
-
删除特定回复
-
删除特定主人回复
-
添加日志、添加回复、添加主人回复
-
点击左侧好友链接,进入好友的空间
- 文中的静态页面已经提供好的,如果没有去尚硅谷公众号下载,直接使用其样式和静态页面即可。
用户登录信息(一键快速登录)、用户详情信息(后面自己再修改手机号和邮箱等等详细信息) 、 日志 、 回贴 、 主人回复
- 用户登录信息:账号、密码、头像、昵称
- 用户详情信息:真实姓名、星座、血型、邮箱、手机号…
- 日志:标题、内容、日期、作者
- 回复:内容、日期、作者、日志
- 主人回复:内容、日期、作者、回复
- 用户登录信息 : 用户详情信息 1:1 PK(一对一关系)
- 用户 : 日志 1:N(一对N)
- 日志 : 回复 1:N(一对N)
- 回复 : 主人回复 1:1 UK(一对一)
- 用户 : 好友 M : N(多对多,一个人可以有多个好友,一个人也可以成为多个人的好友)
注意(上图其实是 QQ 空间项目的 ER 设计图): - 实体用矩形表示;
- 实体的属性用椭圆形表示;
- 实体与实体之间的关系用菱形来表示。
- 第一范式:列不可再分。比如说收货地址,上海市xxx街道xxx路xxxx小区,每一个所属地都可以分成一列,这样在同一个地区的,比如都是xxx街道的,他们的数据库存储xxx路就可以用一个数字来代替,这样存储数字比存储汉字节省空间。
- 第二范式:一张表只表达一层含义(只描述一件事情),确保表中的每一列都和主键相关。
- 第三范式:表中的每一列和主键都是直接依赖关系,而不是间接依赖。
参考链接:数据库-----三大范式–详解
一般数据库设计中遵循的规则:
数据库设计的范式和数据库的查询性能很多时候是相悖的,我们需要根据实际的业务情况做一个选择:
- 查询频次不高的情况下,我们更倾向于提高数据库的设计范式,从而提高存储效率;
- 查询频次较高的情形,我们更倾向于牺牲数据库的规范度,降低数据库设计的范式,允许特定的冗余,从而提高查询的性能;
比如这个 QQ空间项目,明明主人回复中的作者,我们可以根据 日志-回复-主人回复 之间的关联关系查到作者信息,为什么我们还要在主人回复中设置作者这一项呢(这样做不满足第三范式)?
- 如果我们没有添加作者这一列,想在主人回复中查询作者,那么我们需要多表连接查询,查询三次查到作者
- 如果我们添加作者这一列,那么我们查询的时候进行单表查询就可以了,查询效率更高。
解释: - 主键尽量使用没有实际业务意义的自增列,这样以后在场景改变的时候需要合并数据库的时候不会发生主键冲突;
5个实体有6张表,因为多对多关联会产生中间第三张表 t_friend 表,表示 t_user_basic 这张表和自己产生关联。
ORM 编程思想:(object relational mapping),有点万事万物皆对象那种意思
- 一个数据表对应一个 java 类
- 表中的一条记录对应 java 类的一个对象
- 表中的一个字段对应 java 类的一个属性
- java 类中的属性名字尽量和数据库的列名相同,如果确实不相同的话,后面写 sql 语句的时候,记得起列的别名。
- 构造器先定义一个空参构造器,后面需要哪几个参数的构造器再回来补;
- 下面每个类我就不放 方法了,记得构造所有属性的 get、set 方法。
日期的继承关系:
父类:java.util.Date 年月日时分秒毫秒
子类:java.sql.Date 年月日
子类:java.sql.Time 时分秒
- 这里是因为 MySQL 8.0 中默认的 data 类型是 ,所以这里要和老师定义的 Data 类型不一致,否则后面会报错,下面所有涉及到日期类型的都替换成 。
接下来,我们大致要完成的事情是:
- 建立 DAO 接口:指定某一类对应的接口应该完成什么功能,然后建立这些接口的实现类;
- 建立业务层:每一个基本类的 DAO 实现之后呢,需要考虑实现业务功能,需要建立 service 接口,然后实现这些接口的功能;这里需要创建配置文件,配置 DAO 层和业务层交互的 bean 节点,以及 DAO 层和 service 层之间的依赖关系(以后 spring 不用自己手动配置了)
- 建立 controller 控制器:业务层建好之后,controller 负责调用 service 层封装好的各种业务功能实现对同一个类的各种 业务流程控制。
实际上开发中,我们都是根据想实现的功能需求来一步一步完善 DAO层、service 层、controller 控制器以及配置文件的。
首先,我们程序运行之后,首先看到的界面是一个登陆界面,界面如下:
输入用户名和密码之后,需要验证是否登陆成功,也就是要去数据库里面查询是否有对应的账号和密码。
login.html 页面设计成一个表单,被中央控制器 DispatcherServlet 拦截之后,根据配置文件的配置跳转到 控制器:
UserController 类中的登陆方法:
- 首先调用 层中的登陆方法进行登陆验证;
- 登陆成功之后要通过 userBasicService 层获取相关好友列表,以及通过 层获取日志列表,这些都是要展示在主页上的;
- 获取到 friendList 和 topicList 之后要调用 userBasic 类的 set 方法设置到 userBasic 对应的属性上去,也就是进行类的关联,其实也就是表的连接;
- 最后,将获取到的 userBasic 类设置到 session 作用域中,方便之后调用,如果想获取 好友列表和日志列表通过 userBasic 来调用就可以了。
所有的实现方法均需要提前在接口中定义规范,这里实现类中再重写这些方法,这里省略接口步骤。
UserBasicServiceImpl 实现:
UserBasicDAOImpl 实现:
- UserBasicService 层调用 DAO 层的 getUserBasic 方法从数据库查到了一组 UserBasic 信息,返回给 UserController 层;
- 如果查到了 UserBasic 信息,也就是它不为空的话,那么我们需要调用UserBasicService 层的 getFriendList 方法获取好友列表,UserBasicService 层调用的是 DAO 层的 getUserBasicList 方法,注意这里从 t_friend 表中获取到的是一系列 fid 值,这个 fid 值对应的是 UserBasic 中的某 id 值;
- 拿到一系列 id 值之后我们要去 t_user_basic 中找这些 id 值对应的是哪些用户,即 UserBasicService 层遍历每一个 fid 值,调用 DAO 层的 getUserBasicById 方法获取到真正对应的所有好友的 UserBasic 信息。
TopicServiceImpl 实现:
TopicDAOImpl 实现:
- UserController 控制器 - topicService 层 - topicDAO 层的 getTopicList 方法,层层调用,功能分离。
-
URL没修改,用的还是 fruitdb,将 工具类中的 地址修改为 ;
-
给 fid 起别名,TopicDAOImpl 实现类中的 getUserBasicList 方法要封装成一个 UserBasic 的 list,这个类里面没有 fid 这个属性,所以要起一个别名;
-
并且在 baseDAO 中的获取别名的方法修改为 , 而不是 rsmd.getColumnName() (获取列的列名);
-
Can not set com.atguigu.qqzone.pojo.UserBasic field com.atguigu.qqzone.pojo.Topic.author to java.lang.Integer 错误,这个错误是什么原因呢?
-
我们之前将从数据库获取到的数据集,获取数据库列名然后将数据集中的某一列设置到这个运行时类的某个属性上,这里报错是我们需要的是一个 UserBasic 类的属性,但是我们获取到的是 Integer 属性的数据,不能把 Integer 属性的数据强制设置上去;
-
如下图所示,topic 表中最后一列存放的是 author 的 id 值,我们其实获取到的 Integer 值是作者的 id 值,那么我们需要将这个 id 值根据构造器方法封装成一个 UserBasic 类的值赋值上去;
-
获取当前字段的类型名称,判断如果是自定义类型,获取这个自定义类型的 Class 对象,然后获取这个类的带 Integer 类型的构造器(这里因为这个项目只有 Integer 类的某一数据列,其他项目不一定),即需要用反射调用这个自定义类的带一个参数的构造方法,创建出这个自定义类的实例对象,然后将实例对象赋值给这个属性。
下面是 baseDAO 中对应的 setValue 方法的修改:
- 这里如果你是 MySQL 8.0 版本的方法,之前设置 pojo 类的时候将时间格式都设置成了 LocalDateTime 类,所以这里判断是否不是自定义类中要多加一个类型判断,即 。
- 用户登录之后跳转到 index.html 页面,其中页面左侧显示好友列表;上端显示欢迎词,如果不是自己的空间,显示超链接:返回自己的空间;中间显示日志列表。
index.html 的各个模块都需要 Thymeleaf 动态渲染之后进行覆盖:
- 如果在 index.html 页面直接去请求的静态页面资源(.html界面),那么并没有执行 super.processTemplate(),也就是 thymeleaf 没有起作用;
- 这里所有的模块要想动态显示界面,需要设置访问路径为 ,目的是执行 super.processTemplate() 方法,让 thymeleaf 生效,数据动态的显示在界面上。
- 解决方法是添加一个 PageController 控制器,添加page方法。
这里的 PageController 是通用的,所有的静态页面上的动态数据均需要经过 PageController 的作用:
- 通过 servletPath 解析后从 bean 容器中找到 ;
- operate=page 找到 pageController 中的 方法;
- 请求的 page=login 代表获取到的 page 参数就是 login 参数在中央控制器反射调用方法时进行注入;
- pageController 中的 page 方法直接就将 login 这个字符串返回,字符串 返回给 DispatcherServlet;
- 由于没有前缀默认执行的 processTemplate() 方法,login 被 thymeleaf 渲染之后实际上找到的是动态的 页面。
left.html 页面显示,遍历好友列表 并且动态渲染到界面上,界面上显示好友名称即 :
main.html 页面,判断 是否为空,如果不为空则遍历将每一项显示到界面上 ,在界面上显示日志ID 、日志标题 、日志日期 、日志操作(未完成)
- 首先我们要做一个判断,判断是否是自己的界面,如果是,显示欢迎进入xxx的空间 ,如果不是,我们要右边显示一块超链接是返回自己的空间;
- 这里我们如何判断是自己空间还是朋友的空间呢?判断进入的是否是自己的空间的依据是: userBasic 和 friend 这两个 key 中保存的 UserBasic 是否一致, ;
-
第一次发送请求:
- 通过 servletPath 解析后从 bean 容器中找到 pageController;
- operate=page 找到 pageController 中的 page 方法;
- 请求的 page=login 代表获取到的参数就是 String login 参数在中央控制器反射调用方法时进行注入;
- pageController 中的 page 方法直接就将 login 这个字符串返回;
- 由于没有前缀默认执行的 processTemplate() 方法,login 被 thymeleaf 渲染之后实际上找到的是动态的 /login.html 页面。
-
将 login.html 页面响应给客户端:
客户端看到一个登陆页面,我们点击登陆之后,发送了第二次请求。 -
第二次请求(登陆验证):
- /user.do?operate=login,找到配置文件中 id 为 user 的,它对应的类为 UserController 类;
- 又因为 operate=login,调用其中的 login 方法;
- login 方法首先是会调用 userBasicService 组件返回一个 userBasic 对象;
- 再一次调用 userBasicService 组件获取所有的好友列表 friendList,并通过 userBasic 的 set 方法将它设置进这个对象的属性中;
- 然后调用 topicService 获取所有的日志列表 topicList,同样 set 进 userBasic 中;
- 然后将 userBasic 保存到 session 作用域;
- 跳转到 index 页面,其中搭载着 top、left、main 三个 src 路径,响应给客户端。
-
客户端看到这三个 src 之后给服务器发送三次请求,分别请求这三个页面:
后面的三次请求的 后端的处理的具体过程和第一次发送请求的处理过程一样。
- 点击 left.html 左侧的好友,跳转到对应好友的空间,显示好友的日志列表;
- 首先要在 left.html 中设置一个超链接,这里我们遍历的是 这个列表然后将这个信息作为 出现,在遍历的内部可以直接根据 friend 这个参数来调用它的一些属性,比如 ;
- 在 UserController 中新建一个 friend 方法,用来获取对应好友 id 的 userBasic 属性为 currFriend 对象,并且获取它的日志列表 set 进 currFriend 中;
- 将这个 currFriend 放进 session 作用域 friend 对应的 value 中;
UserController 类中获取好友的基本信息,以及好友的日志列表方法:
问题:
- 但是我们一点击好友,在左侧(left)中显示整个 index 页面,修改 target 显示到整个页面上层,在超链接标签 里面加 属性,上面返回自己空间的超链接也需要设置这个属性。
- 已知 topic 的 id,需要根据 topic 的 id 获取特定 topic,首先将显示的 这一行设置一个超链接:
- 新建一个 ,实现 topicDetail 方法,并且是根据 id 值来获取 Topic 日志;
TopicController 控制器的实现:
- 这里要 ,保证找渲染完之后找资源的时候去 frames 文件夹下去找 detail.html 资源。
- 要在 中添加一个 getTopicById 方法,并在实现类中实现它,并且保存到 session 作用域中 key 为 topic 的 value 中;
TopicDAO 实现:
- 同时有一个 topic 的 key,需要在配置文件中进行配置,配置一个 topic 的 bean,并且添加它和 topicService 的依赖关系;
- 新建一个 层,新建 层,并且实现这两个中的方法;
- 在 层中需要用到这个 replyService 中的 getReplyListByTopicId 方法来获取所有的 replyList;
- 但是如果有的 reply 有 hostReply 的话,我们需要一并获取出来,所以新建 层和 层,并且实现其中根据 replyId 查询 hostReply 列表的方法。
TopicController 实现:
- 控制器只需要调用 层实现获取日志列表这个功能,具体实现查询其关联的 reply 列表和 hostReply 列表由 service 层来实现;
- 获取到的 topic 中的 author 只有 id,那么需要在 的 getTopic 方法中封装,在查询 topic 本身信息时,同时调用 中的获取 userBasic 方法,给 author 属性赋值;
- 同理,在 reply 类中也有 author,而且这个 author 也是只有 id,那么我们也需要根据 id 查询得到 author,最后设置关联。
TopicService 实现:
- 将 topic 类其中关联的 replyList 和 author 属性查出来并设置上去,调用 层查询 replyList 属性,调用 层查询 author 属性;
- 又因为 topic 这个数据库表中存放的只有 author 的 id ,所以我们方法是 getUserBasicById;
- 遍历这个 replyList 中的每一条 reply,调用 层和 层将其关联的作者信息和主人回复信息获取到并且设置进去,然后这里是根据 topic 查询的 replyList,所以其中的 topic 是本身就有值的,不用我们 set ;
ReplyDAO 层实现:
- 由于在 t_reply 这个表中关联的 topic 只存储了 topicId 信息,所以我们要根据指定的 topic 先 get 它的 id,再根据这个 topic 将其关联的 reply 列表获取到;
HostReplyService 层实现:
HostReplyDAO 层实现:
- 某 service 层尽量调用别的业务封装好的 service 层,而不用别的 DAO 层,不需要关心其他 DAO 层的实现细节;
- 记得配置配置文件中的 bean 节点和关联信息。
- ,代表 reply 类中缺少空参构造器;
- ,记得将 reply 和 hostReply 中的这个有关 Datatime 即日期类型都要进行修改,并且修改对应的 get/set 方法。
- 我们现在已经获取到了 topic 信息,以及它关联到的所有的 reply 和 hostreply 信息,我们要在页面上把它展示出来;
- 显示图片:
- 显示图片下面的名字:
- 显示标题:
- 显示时间:
- 显示日志内容:
detail.html 实现:
- 显示回复的人的头像,名字,回复的是哪一条留言,该条留言的时间,下-面是回复的内容;
- 回复是一个列表,要进行迭代:
- 回复人的头像:
- 回复人的名字:
- 回复的是哪一条留言:
- 该条回复的时间:
- 回复的内容:
- 如果该条回复有主人回复,显示主人回复的内容和时间;
- 判断如果有主人回复:
- 主人回复的内容:
- 主人回复的时间:
- 如果该条没有主人回复,鼠标放上去显示一个主人回复的超链接,鼠标挪开超链接消失。
- 判断没有主人回复:
- 设置主人回复的超链接(如果在自己空间且该条回复没有主人回复的时候显示), 保证该条回复对应的 id 唯一:
- 首先这是一个表单:;
- 其次我们要有一个添加回复的 operate,回复的标题是我们当前日志的标题:;
-
然后我们需要新建一个 ,其中有一个 addReply 方法,看一下 reply 表中有哪些参数是需要从表单的;
解释:- reply中有 id(自增)、context(需要从表单获取的)、replyDate(当前日期)、author(当前登录的这个作者)、topic(可以从session中获取,也可以给表单设置隐藏域,)
- 那么我们需要获取的参数有:String content ,Integer topicId , HttpSession session
-
同时需要调用 层的 addReply 方法,replyService 同样要调用 层的 addReply 方法和数据库交互;
- 对于 LocalDateTime 类,其中新建当前时间的方法为 ;
- 在 reply 类中创建一个这四个参数的构造器。
- 将各层之间的配置文件配置好;
- 现在数据库更新数据之后,我们要重定向,重新发送一次请求请求数据库的最新的日志详情数据;
- main.html 页面这里请求的 topic 页面详情超链接是:
- 那么我们重定向的页面就是,这样也可以直接跳转到展示日志详情页面上:
- 删除回复功能:如果回复有关联的主人回复,需要先删除主人回复, 因为 对数据库来说,如果需要删除主表数据,需要首先删除子表数据;涉及到和数据库交互,删除数据库的回复,所以要给 发送请求;
- replyController 中添加 delReply 方法,要调用 中的 delReply 方法;
- ReplyService 要判断其中是否有关联的 hostReply ,如果有,需要额外多家一步调用 层的删除 hostReply 方法;
- 然后调用 中的 delReply 方法删除掉 reply 列表;
- 重定向的时候需要 topic 值,所以我们要么从 session 中获取,要么点击删除图标即发送请求时一起传进来;
- 我在自己空间可以删除回复,或者我不在自己空间,但是我可以删除别人空间中我的回复;只需要在删除小图标上做判断 ;
- 添加 delReply 动态功能:当点击删除小图标的时候弹出确认框,
- 删除之后跳转页面超链接为 reply.do,那我们的请求能找到 replyController 控制器,执行其中的 delReply 方法;
- 根据其中的 replyId 确认删除的是哪一条回复;
- 根据其中的 topicId 确认删除之后重定向到哪个日志详情页面。
- 在主页面 main.html 页面上设置删除小按钮,如果不是自己的空间,则不能删除日志,即不显示这个小按钮:
- 设置动态确认框,是否删除日志确认框(属于 JS 范畴):
- 根据确认删除之后跳转链接,我们可以找到 控制器中的 delTopic 方法;
- 删除日志,首先需要考虑是否有关联的回复;删除回复,首先需要考虑是否有关联的主人回复;这些具体的关联细节放到 层去实现;
- 获取该日志关联的所有回复,如果不为空的情况下调用之前的 delReply 方法遍历删除每个回复;
- 删除完之后我们要进行重定向,获取当前用户的所有的日志列表;
TopicController 控制器中获取当前用户的所有 topic 信息:
- 我们删除一篇日志之后,我们只需要更新页面的中间日志列表 main.html 即可,而不用重新请求 index 页面,所以我们根据当前用户获取它关联的所有日志,然后再 set 进 topicList 这个属性中,然后再保存到保存作用域中就可以了。
- 在自己空间别人的回复下面自己没有回复过的显示一个主人回复超链接;
- 点击这个超链接有一个表单显示出来(默认情况下不显示),表单点击提交按钮,给当前的回复添加一个主人回复;
- 在 main.html 界面实现添加新日志功能,但是在别人的空间不显示这个超链接。
这里我尝试了很久,怎么点击超链接之后将隐藏在表格中的表单显示,查了很久也没查到可以先跳转链接再控制 style 显示的方法,所以我就直接设置成只要没有主人回复的话靠近那条回复就显示一个主人回复的框(样式很丑,而且鼠标挪开,框就不见了)。
detail.html 页面实现添加主人回复功能:
HostReplyController 控制器实现:
hostReplyService 层实现:
hostReplyDAO 层实现:
配置文件,其他依赖关系之前都配置过了:
main.html 中右上角显示发表新日志的超链接:
topic.html 表单页面:
TopicController 控制器实现添加新日志功能:
topicService 实现:
topicDAO 层实现: