根据源码,模拟实现 RabbitMQ - 通过 SQLite + MyBatis 设计数据库(2)

目录

一、数据库设计

1.1、数据库选择

1.2、环境配置

1.3、建库建表接口实现

1.4、封装数据库操作

1.5、针对 DataBaseManager 进行单元测试

1.6、心得


一、数据库设计


1.1、数据库选择

MySQL 是我们最熟悉的数据库,但是这里我们选择使用 SQLite,原因如下:

  1. SQLite 比 MySQL 更轻量:一个完整的 SQLite 数据库,只有一个单独的可执行文件(不到 1M).
  2. SQLite 操作简便:SQLite 只是一个本地数据库,相当于是直接操作本地的硬盘.
  3. 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.db
    username:
    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 类,来完成数据库相关的操作,注意细节如下:

  1. 初始化方法:一般谈到初始化,都会用到 构造方法,但是这里我们使用一个 普通的方法 —— init();构造方法一般是用来初始化类的属性,不会涉及到太多的业务逻辑,而此处的初始化,带有业务逻辑,还是单独领出来,手动来调用比较合适.
  2. 建库建表逻辑:这里期望,broker server 启动的时候做出如下逻辑判断:
    1. 如果数据库已经存在(表存在),不做任何操作.
    2. 如果数据库不存在,则建库建表,构造默认数据.

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<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();

    @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<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;
    }

    @Test
    public 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"));

    }

    @Test
    public 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;
    }

    @Test
    public 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"));
    }

    @Test
    public 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;
    }

    @Test
    public 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 最有效的手段.

1.6、心得

做好单元测试真的很重要,在 XXXMapper.xml 中,我在标签中设定了参数 parameterType,如下 

一启动项目就报错( 'sqlSessionFactory' 报错),原因就是 Exchange 中缺少 getter 方法~~

但实际上我并“没有缺少 getter 方法”,因为我再写完属性以后就直接用 IDEA 自动生成 getter/setter 方法了,但是!!! 我往数据库中插入 arguments 时约定为 varchar 类型,因此 setter/getter 方法的无论在 set ,还是 get 时,得到都是 Map<String, Object> 类型,因此项目一启动通过 MyBatis 加载 XXXMapper.xml 文件会直接报错!!!

正确的写法因该是写入/读出数据库,都因该序列化成 JSON 字符串的格式,如下:

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈亦康

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值