Based on the source code, simulate and implement RabbitMQ - design database through SQLite + MyBatis (2)

Table of contents

1. Database design

1.1. Database selection

1.2. Environment configuration

1.3. Implementation of database and table building interface

1.4. Encapsulate database operations

1.5. Unit testing for DataBaseManager

1.6. Experience


1. Database design


1.1. Database selection

MySQL is the database we are most familiar with, but here we choose to use SQLite for the following reasons:

  1. SQLite is more lightweight than MySQL: a complete SQLite database has only a single executable file (less than 1M).
  2. SQLite is easy to operate: SQLite is just a local database, which is equivalent to directly operating the local hard disk.
  3. SQLite is also widely used: on some devices with low performance, SQLite is the first choice for databases, especially mobile terminals and embedded devices (Android system has built-in SQLite).

1.2. Environment configuration

Just use maven directly in java to introduce SQLite dependencies (the version should be considered by yourself) ~

        <!-- https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc -->
        <dependency>
            <groupId>org.xerial</groupId>
            <artifactId>sqlite-jdbc</artifactId>
            <version>3.41.2.1</version>
        </dependency>

The configuration is as follows

spring:
  datasource:
    url: jdbc:sqlite:./data/meta.db
    username:
    password:
    driver-class-name: org.sqlite.JDBC

url: The working path of SQLite, used to store data in a specified file.

username & password: For SQLite, there is no need to use username and password. MySQL is a program with a client-server structure, while SQLite is not a program with a client-server structure and can only be accessed by the local host.

Ps: Although SQLite is different from MySQL, both can be used through frameworks like MyBatis.

1.3. Implementation of database and table building interface

The stored data is: switches, queues, and bindings.

Here we use MyBatis to complete related CRUD.

The mapper interface provides three database and table creation operations and CRUD operations for these three database tables.

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


}

The corresponding implementation is as follows:

    <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. Encapsulate database operations

Here we use customized code to automatically complete the operation of creating databases and tables (in line with the settings of RabbitMQ middleware).

Create the DataBaseManager class to complete database-related operations. Pay attention to the following details:

  1. Initialization method: Generally speaking, when it comes to initialization, the constructor method is used, but here we use a common method - init(); the constructor method is generally used to initialize the attributes of the class and does not involve too much business logic. The initialization here contains business logic, so it is better to get it separately and call it manually.
  2. Database and table building logic: It is expected that the broker server will make the following logical judgments when it starts:
    1. If the database already exists (the table exists), no operation is performed.
    2. If the database does not exist, create a database and tables and construct default data.

Ps: How to determine whether the database exists or not? Just determine whether the meta.db file exists (the url in the configuration file).

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. Unit testing for DataBaseManager

When designing unit tests, the requirement here is that unit test cases and use cases need to be independent of each other and not interfere with each other, such as the following situations:

During the test, insert data a into the database.

When testing b, the data in a may cause interference to b.

Ps: This is not necessarily the database, it may be other aspects, such as whether a file is created, whether a port is occupied...

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



}

Of course, I just designed a simple test case. In fact, from a more rigorous perspective, I need to design richer test cases~

Compared with functional/business code, although test case code is boring to write, it is very important. These operations will greatly improve the development efficiency of the entire project.

Ps: Unit testing is originally a task of development. It is impossible to write code without bugs. Conducting thorough testing is the most effective way to deal with bugs.

1.6. Experience

It is really important to do unit testing well. In XXXMapper.xml, I set the parameter parameterType in the tag, as follows 

An error is reported as soon as the project is started ('sqlSessionFactory' error). The reason is the lack of getter method in Exchange~~

But in fact, I don't "miss any getter methods", because after I finished writing the properties, I directly used IDEA to automatically generate the getter/setter methods, but! ! ! When I insert arguments into the database, they are agreed to be of varchar type. Therefore, whether the setter/getter method is set or get, the type obtained is Map<String, Object>. Therefore, when the project is started, loading the XXXMapper.xml file through MyBatis will directly Report an error! ! !

The correct way to write it is to write/read the database, which should be serialized into a JSON string format, as follows:

 

Guess you like

Origin blog.csdn.net/CYK_byte/article/details/132259884