Kotlin Compose Multiplatform 跨平台(Android端、桌面端)开发实践之使用 SQLDelight 将数据储存至数据库

news2024/11/26 20:30:54

前言

关于标题和文章主题

取标题的时候我还在想,我应该写 Compose 跨平台呢还是写 Kotlin 跨平台。

毕竟对于我的整体项目而言,确实是 Compose 跨平台开发,但是对于我这篇文章要说的东西,那其实也涉及不到多少 Compose 相关的内容,更多的应该是 Kotlin Multiplatform 相关的内容。

二者取舍不下,干脆都写上得了,于是就有了这个读起来怪怪的标题。

前情回顾

很久很久以前,我使用 Compose 写了一个安卓端的计算器 APP:使用 Jetpack Compose 实现一个计算器APP。

其中有一个模式叫做程序员模式,可以很方便的做不同进制之间的计算,所以实际上我自己也经常使用这个 APP 来算一些东西。

特别是上次在写有关串口校验的内容时,经常需要计算二进制和十六进制的数据,还会涉及到位运算。

而众所周知,macOS 自带的计算器非常的 “简洁”,不如 Windows 上的计算器强大。所以我只能使用手机来计算。

显然,这很不方便啊,于是萌发出了将我写的这个计算器移植到桌面端的想法。

非常幸运的,我的计算器使用的是 Compose 来编写 UI 布局,所以几乎可以直接无缝迁移到桌面端上来。

具体迁移过程可以参考我之前写的迁移另外一个 APP 的文章: 跟我一起使用 compose 做一个跨平台的黑白棋游戏(1)整体实现思路、跟我一起使用 compose 做一个跨平台的黑白棋游戏(4)移植到compose-jb实现跨平台。

对于移植这个计算器 APP ,需要重点解决的有两个问题:

  1. 原安卓端程序中我使用了 Jetpack ViewModel 来进行状态管理,但是截止目前,Viewmodel 都还没有移植到 Kotlin Multiplatform 中,并且我没记错的话,官方并没有移植的打算。所以我们需要将原本的 ViewModel 改为使用支持跨平台的状态管理方式。
  2. 原安卓端程序使用了 Room 和 Sqlite 来储存计算历史记录,而 Room 并不支持跨平台,另外,我没记错的话,官方也是没有移植的计划。

对于问题 1 ,在我上面提到的文章中已经给出过解决方案了,所以这里就不再赘述了,我们这篇文章的主题是关于如何使用支持跨平台的数据库 ORM 框架 SQLDelight。

SQLDelight

简介

首先,SQLDelight 是什么东西呢?先来看官方的介绍:

SQLDelight generates typesafe kotlin APIs from your SQL statements. It verifies your schema, statements, and migrations at compile-time and provides IDE features like autocomplete and refactoring which make writing and maintaining SQL simple.

简单说就是一个能让我们在 Kotlin 中使用 SQL 更方便的一个库。如果还是不太理解的话,你可以把它当成支持跨平台的 Room。

虽然它俩好像就没有多少相似之处,哈哈哈。

并且,不像 Room,提供了几个常用的查询语句(Insert、Update、Delete 等),可以直接使用,SQLDelight 的所有查询语句都需要自己手写。

值得一提的是,这个框架是 Square 旗下公司的产品,没错,就是那个开发了 OKHttp、Glide 的公司。

第一步,添加依赖

在开始之前先提一嘴,因为我使用的 Gradle 版本和官网文档中的版本不一样,所以我这里写的添加依赖和官网的不太一样,各位需要根据自己的实际情况来写。

什么?我为什么不用和文档一样的 Gradle 版本?因为官网文档的代码使用的还是老版本的 Gradle 语法……而我为了使用 Compose 只能用新版本的 Gradle……

首先,在项目根目录下的 build.gradle.kts 文件的 plugins 中添加插件依赖:

plugins {
    // ……
    id("app.cash.sqldelight") version "2.0.0-alpha05" apply false
}

这里在末尾添加了 apply false 是因为这个项目是 跨平台 项目,所以插件并不一定所有模块都会用到,所以加上这个表示只是定义需要这个插件以及定义需要的版本,但是并不会实际加载并应用。

显然,我们需要在公共代码模块使用到这个插件,所以转到通用模块(即 common 模块)下的 build.gradle.kts 文件,并在 plugins 中应用插件:

plugins {
    // ……
    id("app.cash.sqldelight")
}

然后,依旧是在通用模块的 build.gradle.kts 文件中添加 SQLDelight 的核心运行库到通用模块源集(sourceSets -> commonMain)中:

    sourceSets {
        val commonMain by getting {
            dependencies {
                // ……
                implementation("app.cash.sqldelight:runtime:2.0.0-alpha05")
            }
        }
      }

最后,需要为不同的平台添加对应的驱动依赖,依旧是在通用模块的 build.gradle.kts 文件中的平台对应(androidMain、desktopMain)源集添加:

    sourceSets {
        val androidMain by getting {
            dependencies {
                // ……
                implementation("app.cash.sqldelight:android-driver:2.0.0-alpha05")
            }
        }
        
        val desktopMain by getting {
            dependencies {
                // ……
                implementation("app.cash.sqldelight:sqlite-driver:2.0.0-alpha05")
            }
        }
      }

需要注意的是,这里的桌面端驱动需要选择 sqlite-driver 这个驱动。

自此,依赖就全部添加完毕。

第二步,编写需要的 SQL 语句

在开始编写 SQL 语句前,我们需要先在 build.gradle.kts 中为 SQLDelight 插件添加一个配置,用于指定从 SQL 语句中生成的 Kotlin 接口代码的名称以及包名之类的信息。

在通用模块下的 build.gradle.kts 文件中添加以下内容:

sqldelight {
    databases {
        create("HistoryDatabase") {
            packageName.set("com.equationl.common.database")
        }
    }
}

其中的 HistoryDatabase 为 SQLdelight 生成的 Kotlin 接口名,com.equationl.common.database 为它的包路径。

比如我这个配置,编译后自动生成的代码路径和名称为:

1.png

完成配置后接下来就是编写 SQL 文件,这个文件将使用 .sq 作为文件后缀。

为了让 Android Studio 或者 IDEA 支持 .sq 的代码高亮和代码提示等,我们可以安装一下 SQLDelight 插件:

2.png

注意这里说的插件是 IDE 的插件,不是 Gradle 插件,不要搞混了。

.sq 文件默认放在和源代码根目录同级目录的 sqldelight 目录下,并且包路径和文件名与上面刚配置的保持一致:

3.png

HistoryDatabase.sq 文件中写入以下内容:

import com.equationl.common.dataModel.Operator;
import kotlin.Int;

CREATE TABLE History (
   id INTEGER AS Int PRIMARY KEY AUTOINCREMENT,
   show_text TEXT,
   left_number TEXT,
   right_number TEXT,
   operator TEXT AS Operator,
   result TEXT,
   create_time INTEGER
);

getAllHistory:
SELECT * FROM History;

insertHistory:
INSERT INTO History(show_text, left_number, right_number, operator, result, create_time)
VALUES ?;

deleteHistory:
DELETE FROM History WHERE History.id == ?;

deleteAllHistory:
DELETE FROM History;

代码不长,我们拆开来一段一段看。

第一部分,如果略懂 SQL 一眼就能看出来,就算不懂 SQL 的也很好理解,就是创建一个名为 History 的表,并且定义了表的字段:

CREATE TABLE History (
   id INTEGER AS Int PRIMARY KEY AUTOINCREMENT,
   show_text TEXT,
   left_number TEXT,
   right_number TEXT,
   operator TEXT AS Operator,
   result TEXT,
   create_time INTEGER
);

其实它就是 SQL 语句,只是增加了一些 SQLDelight 特有的语法,所有 SQL 中有的语法它也能用,例如我们这里没有使用到的 NOT NULL 用于定义字段不能为空之类的。

这里需要注意一下 .sq 中支持的数据类型和 Kotlin 中数据类型的对应关系:

类型在数据库中的类型Kotlin中的类型
INTEGERINTEGERLong
REALREALDouble
TEXTTEXTString
BLOBBLOBByteArray

在我们上面的代码中,有两个字段的定义是这样的:

id INTEGER AS Int PRIMARY KEY AUTOINCREMENT,
operator TEXT AS Operator,

我们把 id 定义为了 INTEGER 类型,它会被转成 kotlin 中的 Long 类型,但是实际上,在我们这个 APP 中, id 应该是 Int 类型的,所以我们使用 AS 关键字将其转为了在 kotlin 中的 Int。

同理,operator 原本是 String 类型,这里我们转成了一个我们自定义的枚举类 Operator

对了,别忘了导入这两个类型,不然会编译失败:

import com.equationl.common.dataModel.Operator;
import kotlin.Int;

可能你会疑问,它怎么知道应该怎么转呢?

答案就是它不知道,所以需要我们自己编写转换函数,这有点类似于 Room 中的 @TypeConverter ,但是这里我们暂时先不说怎么写这个转换函数,不过后面我们会说。

第二部分

getAllHistory:
SELECT * FROM History;

先看第二句,这也是个很简单的 SQL 查询语句,用于查询 History 表中的所有数据。

但是我们在它前面额外的多加了一个不属于 SQL 的语法,这个是 SQLDelight 特有的语法,表示这段 SQL 语句将被编译成名为 getAllHistory 的函数:

7.png

根据这条 SQL 语句内容,这个函数不需要任何参数,且返回值为 Query<History> (可以通过调用 .executeAsList() 转为 List<History>),也就是第一段中定义的表结构,它也会被自动编译成一个 kotlin 中的数据类 data class History:

data class History(
  public val id: Int,
  public val show_text: String?,
  public val left_number: String?,
  public val right_number: String?,
  public val operator_: Operator?,
  public val result: String?,
  public val create_time: Long?,
)

注意:其实这里编译生成的 getAllHistory 还会生成一个带有 mapper 参数的同名函数 ,但是这里为了方便理解,就先不做讲解

假如我们需要按条件查询,例如按指定 id 查询,那也可以这样写:

getHistoryById:
SELECT * FROM History WHERE id == ?;

此时编译生成的 getHistoryById 将会需要传递一个名为 id 的参数:

4.png

这里的代码中的 ? 可以简单理解为需要传递的参数,SQLDelight 在编译生成 kotlin 代码时,会按照语句自动判断它的参数名称。

再来看第三部分

insertHistory:
INSERT INTO History(show_text, left_number, right_number, operator, result, create_time)
VALUES (?, ?, ?, ?, ?, ?);

这个 insertHistory 作用是将数据插入到数据库中,编译生成的函数将需要六个参数:

5.png

这么多参数,你可能会说,怎么这么麻烦,就不能像 Room 那样直接插入数据模型吗?上面不是都说了创建的表会自动生成一个数据类吗?

诶嘿,还真可以,只要这样写:

insertHistory:
INSERT INTO History(show_text, left_number, right_number, operator, result, create_time)
VALUES ?;

生成的函数就是这样的了:

6.png

所以我们就可以直接传入 History 了。

其实讲到这里,后面的函数就不需要我再一一讲解了吧,哈哈。

对了,上面说到的 .sq 自动编译生成的 kotlin 文件在 模块目录/build/generated/sqldelight/code/ 目录下,感兴趣的话可以自己打开看看生成的 Kotlin 文件是什么样子的。

第三步,实现不同平台的驱动

为了使用 SQLDelight ,我们需要适配不同平台的驱动程序。

为此,我们需要用到 kotlin 中的 expectactual 两个关键字。

首先,我们声明一个用于初始化驱动的 except 函数(或类):

// 该段内容位于 common 模块下 commonMain 包中
expect fun createDriver(): SqlDriver

然后在不同的平台代码模块中写上对应的 actual 实现:

// 这段代码位于 common 模块 androdMain 包中
actual fun createDriver(): SqlDriver {
    return AndroidSqliteDriver(HistoryDatabase.Schema, ActivityUtils.getTopActivity(), "history.db")
}

// 这段代码位于 common 模块 desktopMain 包中
actual fun createDriver(): SqlDriver {
    val driver: SqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
    HistoryDatabase.Schema.create(driver)
    return driver
}

注意这里官网和其他教程给出的初始化驱动的代码都是通过一个类来初始化的,因为不同平台可能需要为其提供不同的参数:

// in src/commonMain/kotlin
expect class DriverFactory {
  expect fun createDriver(): SqlDriver
}

// in src/androidMain/kotlin
actual class DriverFactory(private val context: Context) {
  actual fun createDriver(): SqlDriver {
    return AndroidSqliteDriver(Database.Schema, context, "test.db") 
  }
}

// in src/nativeMain/kotlin
actual class DriverFactory {
  actual fun createDriver(): SqlDriver {
    return NativeSqliteDriver(Database.Schema, "test.db")
  }
}

例如在安卓平台中需要提供安卓的上下文 Context ,所以在初始化时需要提供 context 参数,换句话说,涉及到数据库交互的地方可能无法很好的完全实现多个平台通用代码,因为在通用模块中无法拿到安卓上下文,也就无法初始化 SQLDelight 驱动。

但是这里我们这个项目中的业务逻辑几乎全部都写在了通用模块中,对于数据库相关的逻辑我当然也希望能够继续写在通用模块中,好在我的项目可以通过第三方框架拿到安卓的 Context

AndroidSqliteDriver(
   HistoryDatabase.Schema, 
   ActivityUtils.getTopActivity(),  // Context
   "history.db"
)

所以就不存在上面说的这个问题,因此我也就没有以类的形式在获取驱动,而是直接写了一个函数。

最后,注意一下初始化驱动的平台代码,在安卓中返回的是:AndroidSqliteDriver(HistoryDatabase.Schema, ActivityUtils.getTopActivity(), "history.db") 其中第二个参数是 Context,第三个参数是要使用的数据库文件名.

在桌面端则略有不同:

val driver: SqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
HistoryDatabase.Schema.create(driver)
return driver

从代码也能看出,在桌面端我们并没有将数据库写入到文件中,而是将其放到内存中了。这就意味着,当程序退出时数据库中的数据将丢失。

不过由于我们的计算器的逻辑是仿照的微软计算器,而微软计算器的处理逻辑也是历史记录仅在打开程序时有效,关闭程序后记录会被自动清除。

这个逻辑恰好和这里相同了,所以我就直接使用了官网文档中的这种将数据库写入内存的做法。

如果你们想要将数据库写入指定文件的话,可以参考这里:Desktop Compose File Saving using SqlDelight。

最后,在桌面端使用数据库还有一点需要注意,如果我们直接这样运行的话,对于 debug 包确实没有问题,但是如果运行或发布 distributions 包的话会闪退,这是因为在 distributions 包中没有包含所需的数据库支持。

所以我们需要在 desktop 模块下的 build.gradle.kts 文件中的 application -> nativeDistributions 块添加以下代码:

compose.desktop {
    application {
        nativeDistributions {
              // ……
            modules("java.sql")
              // ……
        }
    }
}

第四步,开始使用

在完成了上面的前置准备工作之后,我们就可以开始使用了。

但是为了使用起来更加方便,我们可以自己在 common 模块的 commonMain 包中封装一个帮助类 DataBase。

首先,在这个类中初始化需要使用到的实例:

private val database = HistoryDatabase(
    createDriver(),
    HistoryAdapter = History.Adapter(
        idAdapter = longOfIntAdapter,
        operator_Adapter = stringOfOperatorAdapter
    )
)
private val dbQuery = database.historyDatabaseQueries

在我们后续使用时直接调用 dbQuery.xxx 即可,其中的 xxx 就是我们自己在 .sq 文件中写的那些函数名。

可以看到,在初始化 HistoryDatabase 时,我们提供了两个参数:

  1. 驱动实例,这里的驱动就是上一节中我们所撰写的驱动

  2. 类型转换适配器,文章稍早前有说过,我们把 id 从 Long 转成了 Int 类型,operator 从 String 转成了 Operator 类型,而 SQLDelight 是不知道应该怎么转的,需要我们自己定义转换函数,此时这个参数就是用来定义我们 的转换函数的。

    另外注意,如果我们在 .sq 中没有写需要转换类型的字段的话,这里就没有第二个参数,只需要第一个参数即可。

这两个转换函数是这样定义的,这里以 idAdapter 为例:

private val longOfIntAdapter = object : ColumnAdapter<Int, Long> {
    override fun decode(databaseValue: Long): Int {
        return databaseValue.toInt()
    }

    override fun encode(value: Int): Long {
        return value.toLong()
    }
}

其实也很简单,就是一个匿名函数 ColumnAdapter ,有两个泛型,第一个表示要转换成的类型,第二个表示原本的类型。然后重载 decodeencode 方法,在其中实现类型转换即可。

初始化好数据库实例后,接下里就是写几个方法用于调用数据库查询:

首先是删除行

internal fun delete(historyData: HistoryData?) {
    if (historyData == null) {
        dbQuery.deleteAllHistory()
    }
    else {
        dbQuery.deleteHistory(historyData.id)
    }
}

这里的 dbQuery.deleteAllHistory()dbQuery.deleteHistory(historyData.id) 对应的就是我们前面在 .sq 中写的:

deleteHistory:
DELETE FROM History WHERE History.id == ?;

deleteAllHistory:
DELETE FROM History;

然后是插入数据:

internal fun insert(item: HistoryData) {
    item.run {
        dbQuery.insertHistory(
            History(id, showText, lastInputText, inputText, operator, result, createTime)
        )
    }
}

在这里,其实这个函数的参数可以直接使用 History 类型,这样我们就只需要直接 dbQuery.insertHistory(item) 即可,但是由于我这个项目是迁移自安卓端的,不是新写的,而在安卓端原本的数据模型使用的是自己定义的一个数据类 HistoryData,如果我改用 SQLDelight 生成的 History 的话,就需要改很多地方,所以这里我索性直接在进行数据库查询时转一下得了。

最后,是查询所有记录:

internal fun getAll(): List<HistoryData> {
    return dbQuery.getAllHistory(::mapHistoryList).executeAsList()
}

与上面插入数据函数相同问题,这里 dbQuery.getAllHistory 返回的是 List<History> 数据,而我们需要的是 List<HistoryData> 数据,所以我们需要转一下。

随堂测试,各位还记得上面我们说过的,SQLDelight 生成的 getAllHistory 函数是没有参数的吗?

那么,问题来了,这里传入的参数是什么东西?其实这里的 getAllHistory 不仅有无参的函数,还有一个带有类型为高阶函数的参数的同名函数,且这个高阶函数的参数是所有查询字段的参数。那么这个高阶函数是用来干嘛的呢?当然就是拿来给我们做数据转换或其他处理用的了。

这里的 mapHistoryList 内容如下:

private fun mapHistoryList(
    id: Int,
    show_text: String?,
    left_number: String?,
    right_number: String?,
    operator_: Operator?,
    result: String?,
    create_time: Long?,
): HistoryData {
    return HistoryData(
        id = id,
        showText = show_text ?: "",
        lastInputText = left_number ?: "",
        inputText = right_number ?: "",
        operator = operator_ ?: Operator.NUll,
        result = result ?: "",
        createTime = create_time ?: 0
    )
}

自此,我们的数据库帮助类几乎全部完成了,最后再加一个单例实例方便调用,也避免重复初始化:

companion object {
    val instance by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
        DataBase()
    }
}

完整的 DataBase 类内容如下:

package com.equationl.common.database

import app.cash.sqldelight.ColumnAdapter
import com.equationl.common.dataModel.HistoryData
import com.equationl.common.dataModel.Operator
import com.equationl.common.platform.createDriver

internal class DataBase {

    companion object {
        val instance by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            DataBase()
        }
    }

    private val longOfIntAdapter = object : ColumnAdapter<Int, Long> {
        override fun decode(databaseValue: Long): Int {
            return databaseValue.toInt()
        }

        override fun encode(value: Int): Long {
            return value.toLong()
        }
    }

    private val stringOfOperatorAdapter = object : ColumnAdapter<Operator, String> {
        override fun decode(databaseValue: String): Operator {
            return try {
                Operator.valueOf(databaseValue)
            } catch (e: IllegalArgumentException) {
                Operator.NUll
            }
        }

        override fun encode(value: Operator): String {
            return value.name
        }

    }

    private val database = HistoryDatabase(
        createDriver(),
        HistoryAdapter = History.Adapter(
            idAdapter = longOfIntAdapter,
            operator_Adapter = stringOfOperatorAdapter
        )
    )
    private val dbQuery = database.historyDatabaseQueries

    internal fun delete(historyData: HistoryData?) {
        if (historyData == null) {
            dbQuery.deleteAllHistory()
        }
        else {
            dbQuery.deleteHistory(historyData.id)
        }
    }

    internal fun getAll(): List<HistoryData> {
        return dbQuery.getAllHistory(::mapHistoryList).executeAsList()
    }

    internal fun insert(item: HistoryData) {
        item.run {
            dbQuery.insertHistory(
                History(id, showText, lastInputText, inputText, operator, result, createTime)
            )
        }
    }

    private fun mapHistoryList(
        id: Int,
        show_text: String?,
        left_number: String?,
        right_number: String?,
        operator_: Operator?,
        result: String?,
        create_time: Long?,
    ): HistoryData {
        return HistoryData(
            id = id,
            showText = show_text ?: "",
            lastInputText = left_number ?: "",
            inputText = right_number ?: "",
            operator = operator_ ?: Operator.NUll,
            result = result ?: "",
            createTime = create_time ?: 0
        )
    }


}

最后,在我们实际需要使用到的地方调用即可。

例如,在我这个项目中,我会在点击历史记录图标后从数据库读取历史记录数据并更新到列表中,所以在 ViewModel (注意,这里的 ViewModel 不是 Jetpack ViewModel 框架,只是一个名字)中的 toggleHistory 函数有这么一段代码:

private val dataBase = DataBase.instance

// ……

private fun toggleHistory(forceClose: Boolean, viewStates: MutableState<StandardState>) {
// ……

        CoroutineScope(Dispatchers.IO).launch {
            var list = dataBase.getAll()
            if (list.isEmpty()) {
                list = listOf(
                    HistoryData(-1, showText = "", "null", "null", Operator.NUll, "没有历史记录")
                )
            }
            viewStates.value = viewStates.value.copy(historyList = list)
        }

// ……

}

对了,在这里的代码中,我自己启动了一个协程用于执行数据库查询操作,这是因为对于我这个项目,可以支持我这么做。

而其实 SQLDelight 官方就有支持使用协程执行查询的扩展库,推荐各位还是使用官方的方式来:

val players: Flow<List<HockeyPlayer>> = 
  playerQueries.selectAll()
    .asFlow()
    .mapToList()

依赖: implementation("app.cash.sqldelight:coroutines-extensions:2.0.0-alpha05")

总结

完整项目源码地址:calculator-Compose-MultiPlatform

好了,现在我们已经完全完成了将原本在安卓端使用 Room 实现的数据库储存迁移到了使用 SQLDelight 实现的跨平台数据库储存。

可以看到,其实使用 SQLDelight 也是十分的方便,相较于 Room ,可能也就是前期配置稍微麻烦了那么一点点(毕竟要手写 SQL)。

最最重要的是,SQLDelight 支持跨平台,无论是移动端的 安卓 和 iOS 还是桌面端的 Windows、macOS、Linux,它都支持。

只是在官网教程和其他大佬写的博客中,大多数都是介绍 SQLdelight 在单一平台使用或者移动端跨平台使用,我没看到有介绍跨安卓和桌面端的文章,所以这里我就写了这篇文章。

参考资料

  1. SQLDelight Multiplatform
  2. 【翻译】使用 Ktor 和 SQLDelight 构建跨平台APP教程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/573571.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

力扣sql中等篇练习(二十七)

力扣sql中等篇练习(二十七) 1 连续两年有3个及以上订单的产品 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 # Write your MySQL query statement below WITH T as (SELECT t.product_id,t.d,count(order_id) numFROM(SELECT order_id,product_id,…

es elasticsearch 九 索引index 定制分词器 type结构后期弃用原因 定制动态映射 动态映射模板 零停机重建索引

目录 索引index 定制分词器 Type底层结构及弃用原因 定制 dynamic mapping 定制dynamic mapping template 动态映射模板 零停机重建索引 生产环境应该度别名数据 索引index Put /index Stings 分片 Mapping 映射 Aliases 别名 增加 Put my_index2 { "se…

网络安全行业在经济下行期仍然稳步增长,快抓住风口入行

根据IDC《2022年第四季度中国IT安全软件市场跟踪报告》的数据&#xff0c;2022年下半年中国IT安全软件市场厂商整体收入约为23.8亿美元&#xff08;约合165.7亿元人民币&#xff09;&#xff0c;同比上升12.4%。结合全年数据&#xff0c;2022全年中国IT安全软件市场规模达到39.…

NodeJS 文件操作④

文章目录 ✨文章有误请指正&#xff0c;如果觉得对你有用&#xff0c;请点三连一波&#xff0c;蟹蟹支持&#x1f618;前言NODE内置模块 FS模块 CallBack API mkdir &#xff08;创建文件夹 异步&#xff09; rmdir&#xff08;删除文件夹 异步&#xff09; rm&#…

百度爱番番的线索如何自动导入至CRM系统中?

百度爱番番是什么&#xff1f; 百度爱番番是应用百度强大AI能力&#xff0c;帮助企业实现营销数字化、自动化、智能化&#xff0c;为企业提供拓客、集客、管客的一站式智能解决方案&#xff0c;助力企业营销变得专业且智能。 百度爱番番的线索如何自动导入至CRM系统中&#xf…

MKS SERVO4257D 闭环步进电机_系列2 菜单说明

第1部分 产品介绍 MKS SERVO 28D/35D/42D/57D 系列闭环步进电机是创客基地为满足市场需求而自主研发的一款产品。具备脉冲接口和RS485/CAN串行接口&#xff0c;支持MODBUS-RTU通讯协议&#xff0c;内置高效FOC矢量算法&#xff0c;采用高精度编码器&#xff0c;通过位置反馈&am…

数据结构和算法,在Python中的实现方式

部分数据来源&#xff1a;ChatGPT 数据结构 在计算机科学中&#xff0c;数据结构指的是计算机中用来存储和组织数据的方式。数据结构是为算法服务的&#xff0c;同一个算法在不同的数据结构上运行效率可能会有很大的不同。这就要求我们在解决问题时要根据具体情况选择合适的数…

前端中间件Midway的使用

一、 关于midway1. 解决什么痛点2. 期望达到什么效果 二、创建应用并使用1. 创建midway应用2. 认识Midway2.1 目录结构2.2 Controller2.3 路由2.4 获取请求参数2.5 Web中间件2.6 组件使用2.7 服务(service) 三、写到最后 一、 关于midway Midway 是阿里巴巴 - 淘宝前端架构团队…

别做重复低质的工作内容摸鱼了,18k大佬分享自动化测试秘籍

自动化测试面试真题&#xff08;附答案&#xff09; 一、编程语法题 1、python有哪些数据类型 2、怎么将两个字典合并 3、python 如何将json写到文件里? 4、 __init__和_new__区别? 5、什么是可变、不可变类型&#xff1f; 6、mysql注入点&#xff0c;用工具对目标站直接写入…

IDEA操作数据库并设置时区

目录 设置mysql的时区的方法&#xff08;提供三种选择&#xff09; 1、直接在advanced上配置serverTimezone属性值&#xff08;单次连接有效&#xff09; 2、进入MySQL客户端修改time_zone 3、直接修改MySQL的my.ini配置文件设置time-zone 使用Database 1、查看当前数据源…

uCOSii信号量的作用

uCOSii中信号量的作用&#xff1a; 在创建信号量时&#xff0c;Sem_EventOSSemCreate(1)用于分时复用共享资源&#xff1b; Sem_EventOSSemCreate(0)用于中断和任务间同步或任务之间的同步。 具体在使用时&#xff0c;需要灵活运用。在访问共享资源时&#xff0c;我喜欢用互…

【计算机视觉 | 目标检测】arxiv 计算机视觉关于目标检测的学术速递(5月26日论文合集)

文章目录 一、检测相关(9篇)1.1 Energy-based Detection of Adverse Weather Effects in LiDAR Data1.2 Anomaly Detection with Conditioned Denoising Diffusion Models1.3 Mask Attack Detection Using Vascular-weighted Motion-robust rPPG Signals1.4 Improved Multi-Sca…

AccessShareLock pg cancel backend terminate backend

AccessShareLock 在PostgreSQL中&#xff0c;AccessShareLock是一种用于控制对数据库对象并发访问的锁类型。它是一种读锁&#xff0c;允许多个事务同时从同一个对象进行读取&#xff0c;但它阻止并发事务获取冲突的锁&#xff0c;比如写锁或独占锁。 当一个事务在对象上获取…

【指针的深刻理解】

如何看待下面代码中的a变量? #include<stdio.h> int main() {int a 0;//同样的一个a&#xff0c;在不同的表达式中&#xff0c;名称是一样的&#xff0c;但是含义是完全不同的&#xff01;a 10;//使用的是a的空间&#xff1a;左值int b a; //使用的是a的内容&#x…

Java jiraClient上传附件不能在浏览器预览的问题分析

最近测试的同学反馈问题说使用平台的报bug上传视频的附件以后&#xff0c;然后在jira上确没有办法通过点击附件进行预览&#xff0c;而需要下载下来才能够查看。但是如果是直接在jira上进行上传附件就不会有这个问题 如果说不了解具体原因的&#xff0c;其实就会觉得这个问题非…

【Linux】Top命令参数解释

TOP命令 这是一个Linux系统下 top 命令所输出的进程监控信息。以下是各列含义&#xff1a; top - 09:52:15&#xff1a;当前时间。 up 27 min&#xff1a;系统已经运行的时长。 2 users&#xff1a;当前有2个用户登录到系统上。 load average: 0.97, 0.41, 0.21&#xff1a;系…

Git进阶+Jenkins入门

文章目录 1 Git进阶——GitFlow工作流程1.1 master与develop分支1.1.1 master1.1.2 develop 1.2 feature分支1.3 Release分支1.4 hotfix分支1.1.3 1 Git进阶——GitFlow工作流程 1.1 master与develop分支 1.1.1 master master&#xff1a;发布上线分支&#xff0c;基于master打…

c++调用dll出现LNK2001 无法解析的外部符号

先说说下正常的dll。 动态库显试调用一般3个文件.h .lib .dll &#xff0c;隐式调用 只需要2个文件:.h&#xff08;函数定义&#xff09; .dll 静态库2个文件&#xff1a;.h .lib 先说C正常dll显式调用 #include "BYD_MES/MES2Interface.h" //#include 是以当前…

HTML表单标签form分析

说明&#xff1a;在html的标签中&#xff0c;表单标签与后台联系密切&#xff0c;像用户登录、注册&#xff0c;都是用到页面的表单标签&#xff0c;用户将信息填入到表单中&#xff0c;提交到后端业务中校验处理&#xff0c;再将结果反馈给前端页面。 表单内的标签分别有&…

ChatGPT国内免费使用的方法有哪些?分享几个网内可用的免费的ChatGPT网页版

目录 一、ChatGpt是什么&#xff1f; 二、ChatGPT国内免费使用的方法&#xff1a; 第一点&#xff1a;电脑端 第二点&#xff1a;手机端 三、结语&#xff1a; 一、ChatGpt是什么&#xff1f; ChatGPt是美国OpenAI [1] 研发的聊天机器人程序 。更是人工智能技术驱动的自然语言…