者:面包君
链接:https://zhuanlan.zhihu.com/p/25214682
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
最近在写风控方面的内容,涉及到一些怎么设置网站的用户登陆安全、投资安全、运营安全方面的内容时,正好想起来去年的“月饼门”事件。对于码农来说,写个程序实现脚本抢标,这样的事情其实很简单。正好借着最近Python折腾代码的机会,整理下怎么通过Python来实现抢火车票、红包这些。
需要的工具和组件有:
浏览器驱动ChromeDriver http://chromedriver.storage.googleapis.com/index.html?path=2.20/
Python 3.5
Splinter 执行:pip install splinter安装Splinter即可
重点介绍splinter怎么使用?
>>> from splinter.browser import Browser
>>> xx=Browser(driver_name="chrome")
>>> xx.visit("http://www.zhihu.com/")
介绍几个常用功能:
1. 输入:xx.fill("wd", "dataman")即可在搜索栏搜索dataman。
2. 输入:button=xx.find_by_value(u"提问")
button=xx.find_by_id(u"zu-top-add-questionSBSBSBSBSBSBSB")寻找该按钮
(通过快捷键F12查询)
3. 输入:button.click() 点击该按键
下面用12306抢火车票/京东抢手机来示例下:
#12306秒抢Python代码from splinter.browser import Browserx=Browser(driver_name="chrome")url=“https://kyfw.12306.cn/otn/leftTicket/init”x=Browser(driver_name="chrome")x.visit(url)#填写登陆账户、密码x.find_by_text(u"登录").click()x.fill("loginUserDTO.user_name","your login name")x.fill("userDTO.password","your password")#填写出发点目的地x.cookies.add({"_jc_save_fromStation":"%u4E0A%u6D77%2CSHH"})x.cookies.add({"_jc_save_fromDate":"2016-01-20"})x.cookies.add({u'_jc_save_toStation':'%u6C38%u5DDE%2CAOQ'})#加载查询x.reload()x.find_by_text(u"查询").click()#预定x.find_by_text(u"预订")[1].click()#选择乘客x.find_by_text(u"数据分析侠")[1].click()
#-*- coding:utf-8 -*-#京东抢手机脚本from splinter.browser import Browserimport time#登录页def login(b): #登录京东 b.click_link_by_text("你好,请登录") time.sleep(3) b.fill("loginname","account*****") #填写账户密码 b.fill("nloginpwd","passport*****") b.find_by_id("loginsubmit").click() time.sleep(3) return b#订单页def loop(b): #循环点击 try: if b.title=="订单结算页 -京东商城": b.find_by_text("保存收货人信息").click() b.find_by_text("保存支付及配送方式").click() b.find_by_id("order-submit").click() return b else: #多次抢购操作后,有可能会被转到京东首页,所以要再打开手机主页 b.visit("http://item.jd.com/2707976.html") b.find_by_id("choose-btn-qiang").click() time.sleep(10) loop(b) #递归操作 except Exception as e: #异常情况处理,以免中断程序 b.reload() #重新刷新当前页面,此页面为订单提交页 time.sleep(2) loop(b) #重新调用自己b=Browser(driver_name="chrome") #打开浏览器b.visit("http://item.jd.com/2707976.html")login(b)b.find_by_id("choose-btn-qiang").click() #找到抢购按钮,点击time.sleep(10) #等待10secwhile True: loop(b) if b.is_element_present_by_id("tryBtn"): #订单提交后显示“再次抢购”的话 b.find_by_id("tryBtn").click() #点击再次抢购,进入读秒5,跳转订单页 time.sleep(6.5) elif b.title=="订单结算页 -京东商城": #如果还在订单结算页 b.find_by_id("order-submit").click() else: print('恭喜你,抢购成功') break
系列文章旨在记录和总结自己在Java Web开发之路上的知识点、经验、问题和思考,希望能帮助更多(Java)码农和想成为(Java)码农的人。
上篇文章介绍了数据库应用的开发步骤,一般包括数据库选型、数据库设计、编写数据库访问代码三个步骤。而数据库选型又因开发环境、测试环境和生产环境的不同而不同。
所以,我们在编写数据库访问代码时,要尽量使用符合SQL标准的语法。关于SQL和SQL标准知识,大家可以自行搜索。
因为我们的租房网应用只是用作演示,所以它的数据库选型和数据库设计就从简而行,我就直接使用H2Database(简称H2,可以参考这篇文章)这个数据库作为JDBC的驱动。
要想了解JDBC如何使用,那最权威的资料莫过于JDBC规范了。
JDBC规范的下载与Servlet等规范类似,都可以在JCP官网(https://jcp.org/en/home/index)找到,具体如何下载可以参考这篇文章。
截止到本文成稿之日,JDBC规范的最新版本好像是4.3,对应JSR221,是在2017年完成的。
JDBC规范的内容也不算少,足足有二百多页,包括:
由于涉及内容太多,我们只能介绍JDBC最基本的用法。
上篇文章的结尾我们提到过,JDBC仅仅是一套接口,它的使用模式与接口的普遍性使用模式是类似的。
租房网应用作为使用JDBC的外部程序,它也需要配置一个具体的JDBC实现才行,这就是JDBC驱动。基本上,市面上各个数据库(特别是关系数据库)厂商都会提供自己的JDBC驱动,H2也不会例外。
那如何在租房网应用中添加H2的JDBC驱动的JAR包呢?我们当然可以到H2官网(http://www.h2database.com/html/main.html)去下载它,然后在IDE中配置第三方依赖库(可以参考这篇文章)。
不对,我们不是学习过Maven了吗?可以让Maven来帮助我们管理依赖啊。我们在这篇文章中也已经把租房网应用改造成使用Maven来管理依赖了,比如Spring等依赖。我们当然也可以直接使用Maven来添加H2的JDBC驱动这个依赖啊。
H2的所有东西都在一个JAR包里,当然也包括了它的JDBC驱动了,我们在这篇文章中下载的JAR包是 h2-1.4.200.jar 。
现在,我们像添加Spring依赖那样先到Maven仓库中(https://mvnrepository.com/ )搜索H2这个依赖,后面的在POM文件中添加依赖等具体步骤就不再赘述了,可以参考这篇文章。
H2的依赖在Maven中的描述是这样的:
<!-- https://mvnrepository.com/artifact/com.h2database/h2 --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.200</version> <scope>test</scope> </dependency>
将它复制到我们租房网应用的POM文件中,这样,添加H2的JDBC驱动的JAR包就一切大功告成了,当然,你必须确保你的机器能够连接上互联网,因为Maven会自动到Maven仓库中下载。
其他数据库的JDBC驱动的JAR包也可以同样方式添加到我们的租房网应用中。
在使用JDBC的外部程序(这里就是我们的租房网应用)配置一个JDBC驱动,首先要有该驱动,上面已经解决这个问题了。
然后需要在外部程序中注册该驱动的驱动器类。如何注册呢?事实上,满足 JDBC 4 规范的驱动会自动注册到外部程序中,而我们添加的H2的JDBC驱动是符合 JDBC 4 规范的。因此,这一步就可以跳过了。
事实上,JDBC驱动的这种自动注册机制是使用了JAR规范的一个特性,包含 META-INF/services/java.sql.Driver 文件的JAR文件可以自动注册驱动器类。我们可以用压缩/解压缩工具打开 h2.1.4.200.jar 看看是否有这么个文件,当然,在Eclipse中可以直接看到:
不过,还是介绍一下如何解决JDBC驱动不能自动注册的话该如何手动注册的问题。有两个方法:
Class.forName("org.h2.Driver");
Java -Djdbc.drivers=org.h2.Driver 外部程序的名字
当然,这种方式的变种是在外部程序中编写API接口来设置这个属性:
System.setProperty("jdbc.drivers", "org.h2.Driver");
假设我们的H2的JDBC驱动是不能自动注册的,那么我们应该手动注册它(当然,手动注册也是没有任何问题的,只不过是多此一举而已)。
那么,应该在哪个地方添加手动注册的代码呢?因为目前我们只有在房源服务(HouseService类)里使用到了模拟的房源数据,而未来我们是要将房源数据持久化到数据库中的,所以我们暂且把它加到这个类中吧。
与模拟的房源数据一样,把注册JDBC驱动器类的代码放到HouseService类的构造函数中:
public HouseService() throws ClassNotFoundException { Class.forName("org.h2.Driver"); mockHouses=new ArrayList<House>(); mockHouses.add(new House("1", "金科嘉苑3-2-1201", "详细信息")); mockHouses.add(new House("2", "万科橙9-1-501", "详细信息")); }
当添加 Class.forName() 这一句代码后,可以看到Eclipse自动提示有编译错误:
Unhandled exception type ClassNotFoundException
这是因为Class类的forName()方法会抛出必须要我们处理的异常,如果不处理它,那就继续抛给调用者,因此,我们在构造方法的声明中添加了 throws ClassNotFoundException 这一部分,关于Java异常,我们以后详细介绍。
好,现在让我们在Eclipse中发布租房网应用到Tomcat,并启动Tomcat(可以参考这篇文章),然而在Eclipse的Console(控制台)窗口中却看到众多异常,这些异常形成一条异常链(即最底层的异常抛到外部调用者后,外部调用者捕获后包装成另一个异常再抛给它的外部调用者,如此重复下去,直到最顶层异常被处理后不再往外抛出):
可以看到,最底层的异常就是 Class.forName() 因为找不到 org.h2.Driver 这个类而抛出的异常,随后导致 HouseService 的构造函数抛出异常,因此不能实例化 HouseService ,再导致Spring IoC框架找不到 HouseService 实例来注入到 HouseRenterController 这个Bean,最后导致 dispatcher 这个Servlet初始化失败。
那到底是何原因找不到H2的JDBC驱动器类 org.h2.Driver 呢?原来是POM文件中的H2依赖的配置在作怪,我们必须把 <scope> 标签的内容设置为 compile ,或直接去掉该标签
<!-- https://mvnrepository.com/artifact/com.h2database/h2 --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.200</version> <!-- <scope>test</scope> 必须去掉此标签--> </dependency>
从标签的名字可以得知它是设置某种范围的,配置为 test ,则表示该依赖只在执行测试时有效;配置为 compile (此为默认值),则表示该依赖在执行编译时有效(实际上是任何时候都有效,是程度最强的依赖)。
关于Maven的依赖的相关知识,以后再详细介绍。
现在,我们重新发布应用,启动Tomcat,可以发现一切正常矣!
接下来,我们就可以使用JDBC API来建立与数据库的连接(数据库往往单独部署在独立的数据库服务器上,因此,需要通过网络来访问数据库)。
JDBC API主要被包含在 java.sql 和 javax.sql 这两个包中,在JDK的 rt.jar 中可以找到它们,属于Java的标准(运行时)库。
使用JDBC API中的 DriverManager 类的 getConnection() 方法即可建立与数据库的连接。
getConnection() 方法有多个重载版本(关于重载,可以参考这篇文章),其中一个版本就是:
Connection getConnection(String url, String user, String password) throws SQLException
它返回一个数据库连接,但需要传入一些参数,一般情况下,连接数据库最基本的要求是要知道如下信息:
用户名和密码都有对应的参数,是字符串类型的,显然是由DBA为我们分配的。那这个数据库URL该怎么写呢?
既然我们用的是H2,那我们到它的官网上肯定能找到答案吧。否则的话,我们得到它却不知如何使用,那H2的作者图个啥!费过一番九牛二虎之力后,终于在其官网(http://www.h2database.com/html/main.html)的左侧导航栏中的 Features 中找到了相关内容:
它支持的URL远比我们想象的要复杂的多,但基本格式都是 jdbc:h2: 开头,前缀 jdbc: 实际上是JDBC规范规定的格式,后面的 h2: 当然是指H2Database这个数据库了。
可以看到,H2为我们提供了多种模式,我们可以选择适合我们的模式。但对我们租房网应用来说,或者说对于在开发阶段的应用来说,使用嵌入式模式或者内存模式是最简单的,因为我们无需单独部署数据库服务器。我们这里就直接使用嵌入式模式吧:jdbc:h2:~/h2db/houserenter 。这里没有数据库服务器的IP地址和端口,有的只是一个路径,最后的houserenter就是一个文件,它就代表一个数据库,实际的数据就存在该文件中。当然,文件的格式我们是不知道的也不用知道。
那用户名和密码是什么呢?实际上,这里可以随便填写。但在测试环境和生产环境中,或者数据库是独立部署的环境中,用户名和密码是由DBA负责分配的。
现在,我们的HouseService类的构造函数变成:
public HouseService() throws ClassNotFoundException, SQLException { Class.forName("org.h2.Driver"); String url="jdbc:h2:~/h2db/houserenter"; String user="sa"; String password=""; Connection conn=DriverManager.getConnection(url, user, password); mockHouses=new ArrayList<House>(); mockHouses.add(new House("1", "金科嘉苑3-2-1201", "详细信息")); mockHouses.add(new House("2", "万科橙9-1-501", "详细信息")); }
注意,getConnection()方法也会抛出必须处理的异常 SQLException ,我这选择直接抛给HouseService类的构造函数的调用者。
现在,让我们重新发布应用并启动Tomcat,验证一下是否有问题,结果正常,我们也可以到 ~/h2db (波浪线实际是指你登录操作系统的用户的家目录,在Windows系统中,一般是 C:\Users\用户名\)这个目录下看看是否存在 houserenter 这个文件:
数据库的运维,即数据库服务器的部署和搭建,数据库、表、索引等的创建、授权,数据的备份和迁移,数据的分库分表分区等工作。这些工作往往有专门人员来负责,就是数据库管理员,即DBA。
我们做好数据库选型和设计之后,在测试环境和生产环境中(有的公司开发环境也需要),一般是向DBA申请服务器资源,让他们部署和搭建数据库服务器,建库建表等。
当然,你也可以自己一人包揽所有这些工作。
最后,一定是启动数据库服务器,这样我们的数据库应用才能建立数据库连接,当然必须保证数据库URL是正确的。即数据库连接能够建立的条件必须包括:
而H2支持嵌入式模式,则没有诸多限制,所以在开发环境中使用是最合适不过了!
现在,连接已经建立好了,我们就可以用它来执行SQL语句了。不过,Connection接口并没有执行SQL的方法,而是提供了一个创建Statement对象的方法:
Statement createStatement() throws SQLException
而Statement接口拥有众多执行SQL的方法,最常用的有两个:
int java.sql.Statement.executeUpdate(String sql) throws SQLException ResultSet java.sql.Statement.executeQuery(String sql) throws SQLException
正如方法名字所示,executeUpdate()方法主要是用来执行insert、update或者delete等数据更新(写)操作的SQL语句的(这些语句叫数据操纵语言,即Data Manipulation Language,简称DML)。当然,它还可以执行建立、删除数据表等操作的SQL语句(这些语句叫数据定义语言,即Data Definition Language,简称DDL)。
而executeQuery()方法是用来执行select等数据查询操作的SQL语句的(也属于DML)。它返回查询结果集,我们就可以使用ResultSet接口来访问查询结果了。
大家可以通过IDE或者其他方式查看它们的JavaDoc。
我们之前建立的数据库连接中使用的是 jdbc:h2:~/h2db/houserenter (简称houserenter数据库),这个库中也是H2刚刚建立的,因此该库中还没有任何数据表,所以我们要先使用 create table 语句建立数据表。
根据上篇文章我们对租房网应用的数据库设计,房源表包含三个字段,于是执行SQL的代码如下:
statement.executeUpdate("create table house(id varchar(20) primary key, name varchar(100), detail varchar(500))");
但是,如果我们把它放到HouseService类的构造函数的话,岂不是每次启动应用时都要建立一次house表,大家可以试试,这样的话第二次启动应用时会抛出house表已经存在的异常。
那该怎么办呢?我们可以在每次启动应用时都使用 drop table 语句将原来的house表删除,然后再执行建表语句:
statement.executeUpdate("drop table if exists house");
接着,我们再使用 insert 语句插入模拟的房源数据(因为我们的租房网应用还没有提供发布房源的功能):
statement.executeUpdate("insert into house values('1', '金科嘉苑3-2-1201', '详细信息') "); statement.executeUpdate("insert into house values('2', '万科橙9-1-501', '详细信息') ");
现在,我们的HouseService类的构造方法变成这样:
public HouseService() throws ClassNotFoundException, SQLException { Class.forName("org.h2.Driver"); String url="jdbc:h2:~/h2db/houserenter"; String user="sa"; String password=""; Connection conn=DriverManager.getConnection(url, user, password); Statement statement=conn.createStatement(); statement.executeUpdate("drop table if exists house"); statement.executeUpdate("create table house(id varchar(20) primary key, name varchar(100), detail varchar(500))"); statement.executeUpdate("insert into house values('1', '金科嘉苑3-2-1201', '详细信息') "); statement.executeUpdate("insert into house values('2', '万科橙9-1-501', '详细信息') "); mockHouses=new ArrayList<House>(); mockHouses.add(new House("1", "金科嘉苑3-2-1201", "详细信息")); mockHouses.add(new House("2", "万科橙9-1-501", "详细信息")); }
executeUpdate()方法的返回值是一个整型值,表示执行该SQL语句后受影响的行数,大家可以在上述代码的基础上打印每一次executeUpdate()方法的返回值,看看返回值是多少。
这样,我们的数据准备已经完成,下面应该使用executeQuery()方法来改造查询房源的方法了,原来的是这样的:
public House findHouseById(String houseId) { for (House house : mockHouses) { if (houseId.equals(house.getId())) { return house; } } return null; }
首先,我们仍然需要建立数据库连接,创建Statement对象,因为上面使用的都是局部变量。这不就有代码重复了吗?没错,我们需要消除这种重复,不过,我们暂且把这个任务放一边,先重点看看如何查询数据库,如何访问查询结果集。
根据房源ID查询房源的SQL语句很简单:
select id,name,detail from house where id='这里是房源ID'
查询条件房源ID是通过方法的参数传入的,我们可以利用Java的字符串拼接功能,拼成上述SQL语句
ResultSet rs=statement.executeQuery("select id,name,detail from house where id='" + houseId + "'");
要特别注意拼接SQL语句时用到的单引号和双引号(使用PreparedStatement可以避免此现象,还可以提高性能,以后讨论),很容易出错,而且容易让黑客执行SQL注入攻击,这里就不介绍了。
当然,你必须确保house表存在,而且它有 id、name、detail 三列,这也是非常容易出错的地方,很明显,我们的代码与数据库设计紧耦合了,因此,我们需要思考一下怎么样才能解耦!
言归正传,得到结果集之后,我们就可以访问结果集中的数据了。因为查询结果可能是多条记录,或者零条记录(当然,我们这里house表是以房源ID为主键的,而查询条件就是房源ID,因此查询结果要么该房源ID的房源不存在,要么就只能有一条房源),所以必须遍历结果集,ResultSet接口提供的是 next()方法:
while (rs.next()) { return new House(rs.getString("id"), rs.getString("name"), rs.getString("detail")); }
当然,一旦访问到一条房源记录,那就可以直接返回了。
使用next()方法指向结果集的下一条记录后(若无下一条则返回false),就可以用ResultSet接口提供的getXXX()方法来访问每一列的值了,这里的 XXX 可以是各种数据类型,比如Byte、Short、Int、Long、Float、Double、Date、String等等,传入的参数既可以是该列在select语句中的位置,也可以是selcet语句中出现的列名。
于是,我们的查询房源的方法就变为了:
public House findHouseById(String houseId) { try { String url="jdbc:h2:~/h2db/houserenter"; String user="sa"; String password=""; Connection conn=DriverManager.getConnection(url, user, password); Statement statement=conn.createStatement(); ResultSet rs=statement.executeQuery("select id,name,detail from house where id='" + houseId + "'"); while (rs.next()) { return new House(rs.getString("id"), rs.getString("name"), rs.getString("detail")); } } catch (SQLException e) { System.out.println("HouseService findHouseById SQLException: " + e.getMessage() + "! houseId=" + houseId); } return null; }
需要注意的是,我这里采用了另外一种处理异常的方式,即使用 try-catch 语句捕获抛出的必须处理的异常,并处理它(仅仅打印日志而已,往往要把异常的上下文信息打印出来,便于查找问题)。
另外一个查询用户感兴趣房源的方法,也可以做相应的修改,HouseService类的构造方法中原来的添加模拟房源数据的代码也可以删除了,最后的HouseService变成了这样:
package houserenter.service; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Service; import houserenter.entity.House; @Service public class HouseService { public HouseService() throws ClassNotFoundException, SQLException { Class.forName("org.h2.Driver"); String url="jdbc:h2:~/h2db/houserenter"; String user="sa"; String password=""; Connection conn=DriverManager.getConnection(url, user, password); Statement statement=conn.createStatement(); statement.executeUpdate("drop table if exists house"); statement.executeUpdate("create table house(id varchar(20) primary key, name varchar(100), detail varchar(500))"); statement.executeUpdate("insert into house values('1', '金科嘉苑3-2-1201', '详细信息') "); statement.executeUpdate("insert into house values('2', '万科橙9-1-501', '详细信息') "); } public List<House> findHousesInterested(String userName) { // 这里查找该用户感兴趣的房源,省略,改为用模拟数据 List<House> houses=new ArrayList<>(); try { String url="jdbc:h2:~/h2db/houserenter"; String user="sa"; String password=""; Connection conn=DriverManager.getConnection(url, user, password); Statement statement=conn.createStatement(); ResultSet rs=statement.executeQuery("select id,name,detail from house"); while (rs.next()) { houses.add(new House(rs.getString("id"), rs.getString("name"), rs.getString("detail"))); } return houses; } catch (SQLException e) { System.out.println("HouseService findHousesInterested SQLException: " + e.getMessage() + "! userName=" + userName); } return houses; } public House findHouseById(String houseId) { try { String url="jdbc:h2:~/h2db/houserenter"; String user="sa"; String password=""; Connection conn=DriverManager.getConnection(url, user, password); Statement statement=conn.createStatement(); ResultSet rs=statement.executeQuery("select id,name,detail from house where id='" + houseId + "'"); while (rs.next()) { return new House(rs.getString("id"), rs.getString("name"), rs.getString("detail")); } } catch (SQLException e) { System.out.println("HouseService findHouseById SQLException: " + e.getMessage() + "! houseId=" + houseId); } return null; } }
我们可以重新发布应用并启动Tomcat,然后用浏览器访问租房网应用进行验证,基本上没有问题,唯一存在功能上的问题就是,我们编辑的房源没有存到数据库中,所以我们需要为HouseService类增加一个更新房源的方法,具体实现如下:
public void updateHouseById(House house) { try { String url="jdbc:h2:~/h2db/houserenter"; String user="sa"; String password=""; Connection conn=DriverManager.getConnection(url, user, password); Statement statement=conn.createStatement(); statement.executeUpdate("update house set id='" + house.getId() + "', name='" + house.getName() + "', detail='" + house.getDetail() + "' where id='" + house.getId() + "'"); } catch (SQLException e) { System.out.println("HouseService updateHouseById SQLException: " + e.getMessage() + "! house=" + house.toString()); } }
要非常注意各列的类型是字符串,因此SQL语句中各列的值要加上单引号!
而我们的HouseRenterController控制器类的处理房源编辑表单的Handler方法由原来的:
@PostMapping("/house-form.action") public ModelAndView postHouseForm(String userName, House house) throws UnsupportedEncodingException { // 这里需要验证用户是否登录,省略 //更新指定房源的详情 House target=houseService.findHouseById(house.getId()); target.setName(house.getName()); target.setDetail(house.getDetail()); //将请求转发到查找房源详情的动作 ModelAndView mv=new ModelAndView(); mv.setViewName("redirect:house-details.action?userName=" + userName + "&houseId=" + house.getId()); return mv; }
修改为:
@PostMapping("/house-form.action") public ModelAndView postHouseForm(String userName, House house) throws UnsupportedEncodingException { // 这里需要验证用户是否登录,省略 //更新指定房源的详情 houseService.updateHouseById(house); //将请求转发到查找房源详情的动作 ModelAndView mv=new ModelAndView(); mv.setViewName("redirect:house-details.action?userName=" + userName + "&houseId=" + house.getId()); return mv; }
现在,再重新验证一下,编辑房源也没有问题了!
但是,我们使用JDBC的代码还有很多不足:
不管怎样,我们向数据持久化迈出了一步!
pring在2018年9月发布了Spring-Data-JDBC子项目的1.0.0.RELEASE版本(目前版本为1.0.6-RELEASE),Spring-Data-JDBC设计借鉴了DDD,提供了对DDD的支持,包括:
在前面领域设计:聚合与聚合根一文中,通过列子介绍了聚合与聚合根;而在领域设计:领域事件一文中,通过例子介绍了领域事件。
本文结合Spring-Data-JDBC来重写这两个例子,来看一下Spring-Data-JDBC如何对DDD进行支持。
Spring-Data-JDBC项目还较新,文档并不齐全(Spring-Data-JDBC的文档还是以Spring-Data-JPA为基础编写的,依赖还是Spring-Data-JPA,实际不需要Spring-Data-JPA依赖),所以这里给出搭建过程中的注意点。
新建一个maven项目,pom.xml中配置
<!--这里需要引入spring-boot 2.1.0以上,2.0的boot还没有spring-data-jdbc--><parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version></parent><dependencies> <!--引入spring-data-jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency> </dependencies>
开启jdbc支持
@SpringBootApplication @EnableAutoConfiguration @EnableJdbcRepositories // 主要是这个注解,来启动spring-data-jdbc支持@EnableTransactionManagement public class TestConfig { }
领域设计:聚合与聚合根中举了两个列子:
我们先看Order与OrderDetail。
Order与OrderDetail组成了一个聚合,其中Order是聚合根,聚合中的操作都是通过聚合根来完成的。
在Spring-Data-JDBC中如何表示这一层关系呢?
@Getter // 1 @Table("order_info") // 2 public class Order { @Id // 3 private Long recId; private String name; private Set<OrderDetail> orderDetailList=new HashSet<>(); // 4 public Order(String name) { // 5 this.name=name; } // 其它字段略 public void addDetail(String prodName) { // 6 orderDetailList.add(new OrderDetail(prodName)); } } @Getter // 1 public class OrderDetail { @Id // 3 private Long recId; private String prodName; // 其它字段略 OrderDetail(String prodName) { // 7 this.prodName=prodName; } }
根据上面的说明,我们的sql结构如下:
DROP TABLE IF EXISTS `order_info`; CREATE TABLE `order_info` ( `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(11) NOT NULL COMMENT '订单名称', PRIMARY KEY (`rec_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; DROP TABLE IF EXISTS order_detail; CREATE TABLE `order_detail` ( `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `order_info` BIGINT(11) NOT NULL COMMENT '订单主键,由spring-data-jdbc自动维护', `prod_name` varchar(11) NOT NULL COMMENT '产品名称', PRIMARY KEY (`rec_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
对聚合的操作,Spring-Data-JDBC提供了Repository接口,直接实现即可,提供了类似RubyOnRails那样的动态查询方法,不过需要通过Query注解自行编写sql,详见下文。
@Repository public interface OrderRepository extends CrudRepository<Order, Long> { }
这就搞定了,我们编写一个测试,来测试一下:
@RunWith(SpringRunner.class) @SpringBootTest(classes=TestConfig.class) public class OrderTest { @Autowired private OrderRepository orderRepository; @Test public void testInit() { Order order=new Order("测试订单"); order.addDetail("产品1"); order.addDetail("产品2"); Order info=orderRepository.save(order); // 1 Optional<Order> orderInfoOptional=orderRepository.findById(info.getRecId()); // 2 assertEquals(2, orderInfoOptional.get().getOrderDetailList().size()); // 3 } }
产品与产品评论的关系如下:
代码表示就是简单的通过id进行关联。代码如下:
@Getter public class Product { // 1 @Id private Long recId; private String name; public Product(String name) { this.name=name; } // 其它字段略 } @Getter public class ProductComment { @Id private Long recId; private Long productId; // 2 private String content; // 其它字段略 public ProductComment(Long productId, String content) { this.productId=productId; this.content=content; } }
对应的sql如下:
DROP TABLE IF EXISTS `product`; CREATE TABLE `product` ( `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(11) NOT NULL COMMENT '产品名称', PRIMARY KEY (`rec_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; DROP TABLE IF EXISTS product_comment; CREATE TABLE `product_comment` ( `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `product_id` BIGINT(11) NOT NULL COMMENT '产品主键,手动赋值', `content` varchar(11) NOT NULL COMMENT '评论内容', PRIMARY KEY (`rec_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
产品和评论都是聚合根,所以都有各自的仓储类:
@Repository public interface ProductRepository extends CrudRepository<Product, Long> { } @Repository public interface ProductCommentRepository extends CrudRepository<ProductComment, Long> { @Query("select count(1) from product_comment where product_id=:productId") // 1 int countByProductId(@Param("productId") Long productId); // 2 }
熟悉Mybatis的朋友对这段代码应该很眼熟吧!
测试如下:
@RunWith(SpringRunner.class) @SpringBootTest(classes=TestConfig.class) public class ProductTest { @Autowired private ProductRepository productRepository; @Autowired private ProductCommentRepository productCommentRepository; @Test public void testInit() { Product prod=new Product("产品名称"); Product info=productRepository.save(prod); ProductComment comment1=new ProductComment(info.getRecId(), "评论1"); // 1 ProductComment comment2=new ProductComment(info.getRecId(), "评论2"); productCommentRepository.save(comment1); int num=productCommentRepository.countByProductId(info.getRecId()); assertEquals(1, num); productCommentRepository.save(comment2); num=productCommentRepository.countByProductId(info.getRecId()); assertEquals(2, num); productRepository.delete(info); // 2 num=productCommentRepository.countByProductId(info.getRecId()); assertEquals(2, num); } }
从上面的两个例子可以看出:
// 来自spring示例 class Book { ...... private Set<AuthorRef> authors=new HashSet<>(); } @Table("book_author") class AuthorRef { Long authorId; } class Author { ...... String name; }
在领域设计:领域事件一文中使用Spring提供的ApplicationEvent演示了领域事件,这里通过对Order聚合根的扩展,来看看Spring-Data-JDBC对领域事件的支持。
假设上面的Order创建后,需要发送一个领域事件,该如何处理呢?
Spring-Data-JDBC默认提供了5个事件:
那么对于上面的需求,我们不需要创建什么事件,只需要创建一个监听器,来监听AfterSaveEvent事件就可以了。
@Bean public ApplicationListener<AfterSaveEvent> afterSaveEventListener() { return event -> { Object entity=event.getEntity(); if (entity instanceof Order) { Order order=(Order) entity; System.out.println("订单[" + order.getName() + "]保存成功"); } }; }
重新执行上面的OrderTest的测试方法,会得到如下输出:
订单[测试订单]保存成功
如果我们需要自定义事件,该如何处理呢?Spring-Data-JDBC提供了DomainEvents和AfterDomainEventPublication注解:
@Getter public class OrderCreateEvent extends ApplicationEvent { // 1 private String name; public OrderCreateEvent(Object source, String name) { super(source); this.name=name; } } @Getter @Table("order_info") public class Order { ...... @DomainEvents public ApplicationEvent domainEvent() { // 2 return new OrderCreateEvent(this, this.name); } @AfterDomainEventPublication public void postPublish() { // 3 System.out.println("Event published"); } } public class TestConfig { ...... @Bean public ApplicationListener<OrderCreateEvent> orderCreateEventListener() { // 4 return event -> { System.out.println("订单[" + event.getName() + "]保存成功"); }; } }
再次执行上面的OrderTest的测试方法,会得到如下输出:
订单[测试订单]保存成功 // 这是AfterSaveEvent事件触发的 订单[测试订单]保存成功 // 这是自定义事件触发的 Event published
Spring-Data-JDBC在原来Spring事件的基础上进行了增强:
Spring-Data-JDBC的设计借鉴了DDD。本文演示了Spring-Data-JDBC如何对DDD进行支持:
Spring-Data-JDBC还提供了如下功能:
有兴趣可自行参考文档。
*请认真填写需求信息,我们会在24小时内与您取得联系。