目录
一、数据库设计
1.1、数据库选择
1.2、环境配置
1.3、建库建表接口实现
1.4、封装数据库操作
1.5、针对 DataBaseManager 进行单元测试
MySQL 是我们最熟悉的数据库,但是这里我们选择使用 SQLite,原因如下:
在 java 中直接使用 maven 把 SQLite 依赖引入即可(版本自行考虑)~
org.xerial
sqlite-jdbc
3.41.2.1
配置如下
spring:
datasource:
url: jdbc:sqlite:./data/meta.db
username:
password:
driver-class-name: org.sqlite.JDBC
url:SQLite 的工作路径,用来存储数据在某个指定的文件中.
username & password:对于 SQLite 来说,不需要使用 用户名密码. MySQL 是一个客户端服务器结构的程序,而 SQLite 则不是客户端服务器结构的程序,只有本地主机能访问.
Ps:SQLite 虽然和 MySQL 不太一样,但是都可以通过 MyBatis 这样的框架来使用.
存储的数据就是:交换机、队列、绑定.
这里我们使用 MyBatis 来完成相关的 CRUD.
mapper 接口中提供三个建库建表操作和针对这三个库表进行 CRUD 的操作.
@Mapper
public interface MetaMapper {
//三个核心建表方法
void createExchangeTable();
void createQueueTable();
void createBindingTable();
//基于上述三个表,进行 插入、删除、查询 操作
void insertExchange(Exchange exchange);
List selectAllExchange();
void deleteExchange(String exchangeName);
void insertQueue(MSGQueue queue);
List selectAllQueue();
void deleteQueue(String queueName);
void insertBinding(Binding binding);
List selectAllBinding();
void deleteBinding(Binding binding);
}
对应的实现如下:
create table if not exists exchange (
name varchar(50) primary key,
type int,
durable boolean,
autoDelete boolean,
arguments varchar(1024)
);
create table if not exists queue (
name varchar(50) primary key,
durable boolean,
exclusive boolean,
autoDelete boolean,
arguments varchar(1024)
);
create table if not exists binding (
exchangeName varchar(50),
queueName varchar(50),
bindingKey varchar(256)
)
insert into exchange values (#{name}, #{type}, #{durable}, #{autoDelete}, #{arguments});
delete from exchange where name = #{name};
insert into queue values(#{name}, #{durable}, #{exclusive}, #{autoDelete}, #{arguments});
delete from queue where name = #{name};
insert into binding values (#{exchangeName}, #{queueName}, #{bindingKey});
delete from binding where exchangeName = #{exchangeName} and queueName = #{queueName};
这里我们通过定制化 代码 的方式来自动完成建库建表的操作(符合 RabbitMQ 中间件的设定).
创建 DataBaseManager 类,来完成数据库相关的操作,注意细节如下:
Ps:怎么判定数据库存在或者不存在?就判定 meta.db 文件是否存在即可(配置文件中的 url).
public class DataBaseManager {
//这里不使用 Autowired 注解获取,因为当前这个类需要我们后面手动管理
private MetaMapper metaMapper;
//针对数据库进行初始化
public void init() {
//手动获取到 MetaMapper
metaMapper = 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 selectAllExchange() {
return metaMapper.selectAllExchange();
}
public void deleteExchange(String exchangeName) {
metaMapper.deleteExchange(exchangeName);
}
public void insertQueue(MSGQueue queue) {
metaMapper.insertQueue(queue);
}
public List selectAllQueue() {
return metaMapper.selectAllQueue();
}
public void deleteQueue(String queueName) {
metaMapper.deleteQueue(queueName);
}
public void insertBinding(Binding binding) {
metaMapper.insertBinding(binding);
}
public List 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] 数据库目录删除失败!");
}
}
}
设计单元测试,这里的要求就是单元测试用例和用例之间是需要相互独立的,不会干扰,例如以下情况:
测试过程中,向数据库中插入数据 a .
在针对 b 进行测试,可能 a 这里的数据会对 b 造成干扰.
Ps:这里不一定是数据库,也可能是其他方面,例如是否搞了一个文件,是否占用了端口...
@SpringBootTest
public class DataBaseManagerTests {
private DataBaseManager dataBaseManager = new DataBaseManager();
@BeforeEach
public void setUp() {
RabbitmqProjectApplication.context = SpringApplication.run(RabbitmqProjectApplication.class);
dataBaseManager.init();
}
@AfterEach
public void setclose() {
//此处不能直接删除 数据库文件 ,需要先关闭 context 对象
//此处 context 对象持有了 MetaMapper 的实例, MetaMapper 又打开了 meta.db 数据库
//如果 meta.db 被别人打开了,此时删除文件是不会成功的(Windows 系统限制, Linux 则不会)
//另一方面 context 会占用 8080 端口,此处的 close 也是释放 8080 端口
RabbitmqProjectApplication.context.close();
dataBaseManager.deleteDB();
}
@Test
public void testInitTable() {
List exchanges = dataBaseManager.selectAllExchange();
List msgQueues = dataBaseManager.selectAllQueue();
List 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;
}
@Test
public void insertExhangeTest() {
Exchange exchange = createTestExchange("testExchange");
dataBaseManager.insertExchange(exchange);
List 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"));
}
@Test
public void deleteExchangeTest() {
Exchange exchange = createTestExchange("testExchange");
dataBaseManager.insertExchange(exchange);
List 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;
}
@Test
public void testInsertQueue() {
MSGQueue queue = createTestQueue("testQueue");
dataBaseManager.insertQueue(queue);
List 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"));
}
@Test
public void testDeleteQueue() {
MSGQueue queue = createTestQueue("testQueue");
dataBaseManager.insertQueue(queue);
List 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;
}
@Test
public void testInsertAndDeleteBinding() {
Binding binding = createTestBinding("testExchange", "testQueue");
dataBaseManager.insertBinding(binding);
List 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 最有效的手段.