目录
一、数据库设计
1.1、数据库选择
1.2、环境配置
1.3、建库建表接口实现
1.4、封装数据库操作
1.5、针对 DataBaseManager 进行单元测试
一、数据库设计
1.1、数据库选择
MySQL 是我们最熟悉的数据库,但是这里我们选择使用 SQLite,原因如下:
- SQLite 比 MySQL 更轻量:一个完整的 SQLite 数据库,只有一个单独的可执行文件(不到 1M).
- SQLite 操作简便:SQLite 只是一个本地数据库,相当于是直接操作本地的硬盘.
- SQLite 应用也非常广泛:在一些性能不高的设备上,SQLite 是数据库的首选,尤其是移动端和嵌入式设备(Android 系统就是内置的 SQLite).
1.2、环境配置
在 java 中直接使用 maven 把 SQLite 依赖引入即可(版本自行考虑)~
<!-- https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc --><dependency><groupId>org.xerial</groupId><artifactId>sqlite-jdbc</artifactId><version>3.41.2.1</version></dependency>
配置如下
spring:datasource:url: jdbc:sqlite:./data/meta.dbusername:password:driver-class-name: org.sqlite.JDBC
url:SQLite 的工作路径,用来存储数据在某个指定的文件中.
username & password:对于 SQLite 来说,不需要使用 用户名密码. MySQL 是一个客户端服务器结构的程序,而 SQLite 则不是客户端服务器结构的程序,只有本地主机能访问.
Ps:SQLite 虽然和 MySQL 不太一样,但是都可以通过 MyBatis 这样的框架来使用.
1.3、建库建表接口实现
存储的数据就是:交换机、队列、绑定.
这里我们使用 MyBatis 来完成相关的 CRUD.
mapper 接口中提供三个建库建表操作和针对这三个库表进行 CRUD 的操作.
@Mapper
public interface MetaMapper {//三个核心建表方法void createExchangeTable();void createQueueTable();void createBindingTable();//基于上述三个表,进行 插入、删除、查询 操作void insertExchange(Exchange exchange);List<Exchange> selectAllExchange();void deleteExchange(String exchangeName);void insertQueue(MSGQueue queue);List<MSGQueue> selectAllQueue();void deleteQueue(String queueName);void insertBinding(Binding binding);List<Binding> selectAllBinding();void deleteBinding(Binding binding);}
对应的实现如下:
<update id="createExchangeTable">create table if not exists exchange (name varchar(50) primary key,type int,durable boolean,autoDelete boolean,arguments varchar(1024));</update><update id="createQueueTable">create table if not exists queue (name varchar(50) primary key,durable boolean,exclusive boolean,autoDelete boolean,arguments varchar(1024));</update><update id="createBindingTable">create table if not exists binding (exchangeName varchar(50),queueName varchar(50),bindingKey varchar(256))</update><insert id="insertExchange" parameterType="com.example.rabbitmqproject.mqserver.core.Exchange">insert into exchange values (#{name}, #{type}, #{durable}, #{autoDelete}, #{arguments});</insert><select id="selectAllExchange" resultType="com.example.rabbitmqproject.mqserver.core.Exchange">select * from exchange;</select><delete id="deleteExchange" parameterType="com.example.rabbitmqproject.mqserver.core.Exchange">delete from exchange where name = #{name};</delete><insert id="insertQueue" parameterType="com.example.rabbitmqproject.mqserver.core.MSGQueue">insert into queue values(#{name}, #{durable}, #{exclusive}, #{autoDelete}, #{arguments});</insert><select id="selectAllQueue" resultType="com.example.rabbitmqproject.mqserver.core.MSGQueue">select * from queue;</select><delete id="deleteQueue">delete from queue where name = #{name};</delete><insert id="insertBinding">insert into binding values (#{exchangeName}, #{queueName}, #{bindingKey});</insert><select id="selectAllBinding" resultType="com.example.rabbitmqproject.mqserver.core.Binding">select * from binding;</select><delete id="deleteBinding">delete from binding where exchangeName = #{exchangeName} and queueName = #{queueName};</delete>
1.4、封装数据库操作
这里我们通过定制化 代码 的方式来自动完成建库建表的操作(符合 RabbitMQ 中间件的设定).
创建 DataBaseManager 类,来完成数据库相关的操作,注意细节如下:
- 初始化方法:一般谈到初始化,都会用到 构造方法,但是这里我们使用一个 普通的方法 —— init();构造方法一般是用来初始化类的属性,不会涉及到太多的业务逻辑,而此处的初始化,带有业务逻辑,还是单独领出来,手动来调用比较合适.
- 建库建表逻辑:这里期望,broker server 启动的时候做出如下逻辑判断:
- 如果数据库已经存在(表存在),不做任何操作.
- 如果数据库不存在,则建库建表,构造默认数据.
Ps:怎么判定数据库存在或者不存在?就判定 meta.db 文件是否存在即可(配置文件中的 url).
public class DataBaseManager {//这里不使用 Autowired 注解获取,因为当前这个类需要我们后面手动管理private MetaMapper metaMapper;//针对数据库进行初始化public void init() {//手动获取到 MetaMappermetaMapper = RabbitmqProjectApplication.context.getBean(MetaMapper.class);if(!checkDBExists()) {//数据库不存在,就进行建库建表操作//先创建出目录结构(否则会报错:找不到目录结构)File dataDir = new File("./data");dataDir.mkdirs();//创建数据库createTable();//插入默认数据createDefaultData();System.out.println("[DataBaseManager] 数据库初始化完成!");} else {//数据库存在,什么都不做即可System.out.println("[DataBaseManager] 数据库已存在!");}}private boolean checkDBExists() {File file = new File("./data/meta.db");return file.exists();}private void createTable() {metaMapper.createExchangeTable();metaMapper.createQueueTable();metaMapper.createBindingTable();System.out.println("[DataBaseManager] 创建表完成!");}/*** 添加默认交换机* RabbitMQ 有一个这样的设定:带有一个 匿名 的交换机,类型是 Direct*/private void createDefaultData() {Exchange exchange = new Exchange();exchange.setName("");exchange.setType(ExchangeType.DIRECT);exchange.setDurable(true);exchange.setAutoDelete(false);metaMapper.insertExchange(exchange);System.out.println("[DataBaseManager] 创建初始数据完成!");}//把数据库中其他操作也在这里封装一下public void insertExchange(Exchange exchange) {metaMapper.insertExchange(exchange);}public List<Exchange> selectAllExchange() {return metaMapper.selectAllExchange();}public void deleteExchange(String exchangeName) {metaMapper.deleteExchange(exchangeName);}public void insertQueue(MSGQueue queue) {metaMapper.insertQueue(queue);}public List<MSGQueue> selectAllQueue() {return metaMapper.selectAllQueue();}public void deleteQueue(String queueName) {metaMapper.deleteQueue(queueName);}public void insertBinding(Binding binding) {metaMapper.insertBinding(binding);}public List<Binding> selectAllBinding() {return metaMapper.selectAllBinding();}public void deleteBinding(Binding binding) {metaMapper.deleteBinding(binding);}public void deleteDB() {//删除文件File file = new File("./data/meta.db");boolean res = file.delete();if(res) {System.out.println("[DataBaseManager] 数据库文件删除完毕!");} else {System.out.println("[DataBaseManager] 数据库文件删除失败!");}//删除目录File dataDir = new File("./data");boolean ret = dataDir.delete();if(ret) {System.out.println("[DataBaseManager] 数据库目录删除完成!");} else {System.out.println("[DataBaseManager] 数据库目录删除失败!");}}}
1.5、针对 DataBaseManager 进行单元测试
设计单元测试,这里的要求就是单元测试用例和用例之间是需要相互独立的,不会干扰,例如以下情况:
测试过程中,向数据库中插入数据 a .
在针对 b 进行测试,可能 a 这里的数据会对 b 造成干扰.
Ps:这里不一定是数据库,也可能是其他方面,例如是否搞了一个文件,是否占用了端口...
@SpringBootTest
public class DataBaseManagerTests {private DataBaseManager dataBaseManager = new DataBaseManager();@BeforeEachpublic void setUp() {RabbitmqProjectApplication.context = SpringApplication.run(RabbitmqProjectApplication.class);dataBaseManager.init();}@AfterEachpublic void setclose() {//此处不能直接删除 数据库文件 ,需要先关闭 context 对象//此处 context 对象持有了 MetaMapper 的实例, MetaMapper 又打开了 meta.db 数据库//如果 meta.db 被别人打开了,此时删除文件是不会成功的(Windows 系统限制, Linux 则不会)//另一方面 context 会占用 8080 端口,此处的 close 也是释放 8080 端口RabbitmqProjectApplication.context.close();dataBaseManager.deleteDB();}@Testpublic void testInitTable() {List<Exchange> exchanges = dataBaseManager.selectAllExchange();List<MSGQueue> msgQueues = dataBaseManager.selectAllQueue();List<Binding> bindings = dataBaseManager.selectAllBinding();Assertions.assertEquals(1, exchanges.size());Assertions.assertEquals("", exchanges.get(0).getName());Assertions.assertEquals(ExchangeType.DIRECT, exchanges.get(0).getType());Assertions.assertEquals(0, msgQueues.size());Assertions.assertEquals(0, bindings.size());}private Exchange createTestExchange(String exchangeName) {Exchange exchange = new Exchange();exchange.setName(exchangeName);exchange.setType(ExchangeType.FANOUT);exchange.setDurable(true);exchange.setAutoDelete(false);exchange.setArguments("aaa", 1);exchange.setArguments("bbb", 2);return exchange;}@Testpublic void insertExhangeTest() {Exchange exchange = createTestExchange("testExchange");dataBaseManager.insertExchange(exchange);List<Exchange> exchanges = dataBaseManager.selectAllExchange();Assertions.assertEquals(2, exchanges.size());Exchange testExchange = exchanges.get(1);Assertions.assertEquals("testExchange", testExchange.getName());Assertions.assertEquals(ExchangeType.FANOUT, testExchange.getType());Assertions.assertEquals(true, testExchange.isDurable());Assertions.assertEquals(false, testExchange.isAutoDelete());Assertions.assertEquals(1, testExchange.getArguments("aaa"));Assertions.assertEquals(2, testExchange.getArguments("bbb"));}@Testpublic void deleteExchangeTest() {Exchange exchange = createTestExchange("testExchange");dataBaseManager.insertExchange(exchange);List<Exchange> exchanges = dataBaseManager.selectAllExchange();Assertions.assertEquals(2, exchanges.size());Assertions.assertEquals("testExchange", exchanges.get(1).getName());//删除dataBaseManager.deleteExchange("testExchange");exchanges = dataBaseManager.selectAllExchange();Assertions.assertEquals(1, exchanges.size());}private MSGQueue createTestQueue(String queueName) {MSGQueue queue = new MSGQueue();queue.setName(queueName);queue.setDurable(true);queue.setExclusive(false);queue.setAutoDelete(false);queue.setArguments("aaa", 1);queue.setArguments("bbb", 2);return queue;}@Testpublic void testInsertQueue() {MSGQueue queue = createTestQueue("testQueue");dataBaseManager.insertQueue(queue);List<MSGQueue> queues = dataBaseManager.selectAllQueue();Assertions.assertEquals(1, queues.size());MSGQueue msgQueue = queues.get(0);Assertions.assertEquals("testQueue", msgQueue.getName());Assertions.assertEquals(true, msgQueue.isDurable());Assertions.assertEquals(false, msgQueue.isExclusive());Assertions.assertEquals(false, msgQueue.isAutoDelete());Assertions.assertEquals(1, msgQueue.getArguments("aaa"));Assertions.assertEquals(2, msgQueue.getArguments("bbb"));}@Testpublic void testDeleteQueue() {MSGQueue queue = createTestQueue("testQueue");dataBaseManager.insertQueue(queue);List<MSGQueue> queues = dataBaseManager.selectAllQueue();Assertions.assertEquals(1, queues.size());//删除dataBaseManager.deleteQueue("testQueue");queues = dataBaseManager.selectAllQueue();Assertions.assertEquals(0, queues.size());}private Binding createTestBinding(String exchangeName, String queueName) {Binding binding = new Binding();binding.setExchangeName(exchangeName);binding.setQueueName(queueName);binding.setBindingKey("testBindingKey");return binding;}@Testpublic void testInsertAndDeleteBinding() {Binding binding = createTestBinding("testExchange", "testQueue");dataBaseManager.insertBinding(binding);List<Binding> bindingList = dataBaseManager.selectAllBinding();Assertions.assertEquals(1, bindingList.size());binding = bindingList.get(0);Assertions.assertEquals("testExchange", binding.getExchangeName());Assertions.assertEquals("testQueue", binding.getQueueName());Assertions.assertEquals("testBindingKey", binding.getBindingKey());//删除dataBaseManager.deleteBinding(binding);bindingList = dataBaseManager.selectAllBinding();Assertions.assertEquals(0, bindingList.size());}}
当然,我只是做了简单的设计测试用例,实际上站在更严谨的角度,还需要设计更丰富的测试用例~
相比于 功能/业务代码,测试用例代码编写起来虽然比较无聊,但是重要性是非常大的,这些操作会大大提高整个项目的开发效率.
Ps:单元测试,本来就是开发要搞的活,写代码不可能没有 bug,进行周密的测试,是应对 bug 最有效的手段.