Metabase 数据库驱动开发基础:从四大核心职责到模块化插件化落地
2026/9/10 14:29:45 网站建设 项目流程

Metabase 数据库驱动开发基础:从四大核心职责到模块化插件化落地

【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase

Metabase 的数据库驱动(Driver)是其开放架构的根基——它让 Metabase 能以统一的方式连接 SQLite、ClickHouse、MongoDB 乃至自定义数据源。本文以官方《Database driver basics》为骨架,结合当前仓库源码,系统讲解驱动的四大核心职责、模块/插件二段式组织方式、插件清单(manifest)语法、multimethod 实现要点与测试扩展机制,帮助你从零搭建并打包一个可运行的 Metabase 驱动。

读完本文,你将掌握:Metabase 驱动究竟承担哪些工作、驱动模块的文件组织与依赖声明、如何把驱动构建为插件 JAR 并放入plugins目录、以及如何通过测试扩展让驱动通过 Metabase 的共享测试套件。

一个 Metabase 驱动到底做了什么:四大核心职责

Metabase 官方文档将驱动定义为四个核心职责的总和,仓库源码src/metabase/driver.clj中的一系列 multimethod 正是这四大职责的落点:

  1. 为 Metabase 提供数据库的基本信息——包括数据库的能力(capabilities)、连接属性(connection properties)等。 对应实现:display-name(src/metabase/driver.clj#L206)与connection-properties(src/metabase/driver.clj#L618)等 multimethod。

  2. 为 Metabase 提供数据库的 schema 信息——表(或等价物)、表中的字段、外键关系(对支持外键的数据库而言)。 该能力服务于 Metabase 的sync 过程(详见 数据库同步与扫描),同步结果存入应用数据库;可视化查询构建器(Query Builder)等界面正是基于这些元数据向用户展示可用的表/列。

  3. 把 Metabase 自研的查询语言 MBQL(Metabase BI Query Language)编译为原生查询。 可视化查询构建器生成 MBQL 查询,Metabase 的query processor负责把 MBQL 翻译成原生查询。对应 multimethodmbql->native(src/metabase/driver.clj#L1135),其 docstring 明确要求返回符合:metabase.query-processor.compile/compiledschema 的编译结果,例如 PostgreSQL 驱动返回{:query "SELECT * FROM my_table"}形式的 SQL 文本。

  4. 执行原生查询并返回结果。 对应 multimethodexecute-reducible-query(src/metabase/driver.clj#L641),docstring 要求返回可通过transduce/reduce消费的行数据流,并借助metabase.query-processor.reducible/reducible-rows实现流式结果。

值得一提的是,这四个职责并非每个驱动都要从零实现——绝大多数逻辑由共享的父驱动(如:sql-jdbc)预先完成,驱动作者只需按需覆盖(详见后文"父驱动"一节)。

把驱动写成模块、打包成插件

Metabase 驱动采用"模块 + 插件"的二段式组织方式:

  • 模块(module)是源码。一个驱动模块就是一个独立的 Clojure 项目目录,包含自己的deps.edn、源码、资源文件与测试。
  • 插件(plugin)是从源码构建出的 JAR。一个 Metabase 插件 JAR 内包含编译后的 class 文件,以及一份声明插件元数据的 Metabase 插件清单 metabase-plugin.yaml。

懒加载机制

在绝大多数情况下,插件是**懒加载(lazily loaded)**的:Metabase 启动时不会立即初始化驱动,而是等到第一次有人尝试连接使用该驱动的数据库时才完成初始化。这能显著缩短启动时间并减少内存占用——这也是官方建议保持lazy-load: true的原因。

安装插件:放入 plugins 目录

要让 Metabase 使用你的驱动,只需把构建好的驱动 JAR 放入/plugin目录——该目录位于你运行metabase.jar的同一位置,目录结构类似:

/Users/cam/metabase/metabase.jar /Users/cam/metabase/plugins/my-plugin.jar

注意:官方文档此处写作/plugin,而实际默认目录名为plugins。你可以通过设置环境变量MB_PLUGINS_DIR修改插件目录。

MB_PLUGINS_DIR 环境变量

关于MB_PLUGINS_DIR,仓库的环境变量文档(docs/configuring-metabase/environment-variables.md#L3444)给出了精确定义:

  • 类型:string
  • 默认值"plugins"
  • 语义:存放 Metabase 数据库驱动的 "plugins" 目录路径。运行 JAR 时默认目录是plugins,创建于 JAR 文件同一位置;以 Docker 方式运行时默认目录为/plugins。运行 Metabase 的用户应拥有对该目录的写权限。
  • 用途:自定义第三方驱动应放置于此,Metabase 启动时会加载该目录下的驱动,可在日志中确认加载结果。

从当前仓库看真实的驱动模块布局

当前仓库中,核心的 SQLite 驱动位于 src/metabase/driver/sqlite.clj(以(driver/register! :sqlite, :parent #{:sql-jdbc})注册),而modules/drivers目录下则聚合了以模块形式发布的第三方/独立驱动:athena、bigquery-cloud-sdk、clickhouse、databricks、druid-jdbc、hive-like、mongo、oracle、presto-jdbc、redshift、snowflake、sparksql、sqlserver、starburst、vertica。这些模块通过 modules/drivers/deps.edn 统一聚合,每个模块以:local/root形式声明为依赖,例如metabase/clickhouse {:local/root "clickhouse"}。当你把新驱动作为模块合入时,同样需要在modules/drivers/deps.edn中登记。

移除一个驱动

若要从 Metabase 中移除某个驱动模块,官方给出了两个步骤:

  1. 删除modules/drivers下对应的驱动文件夹;
  2. modules/drivers/deps.edn中删除该驱动的依赖条目。

特别注意:Postgres、H2、MySQL 三个驱动不可移除——Metabase 需要它们作为应用数据库(application database)的连接驱动。

一个驱动模块的目录解剖:以 SQLite 为例

官方文档以 SQLite 驱动为例给出了模块的标准目录结构:

|-- deps.edn |-- resources | `-- metabase-plugin.yaml |-- src | `-- metabase | `-- driver | `-- sqlite.clj `-- test `-- metabase |-- driver | `-- sqlite_test.clj `-- test `-- data `-- sqlite.clj

对照当前仓库,SQLite 驱动的核心实现在 src/metabase/driver/sqlite.clj(共 607 行),而 ClickHouse 驱动则完整地演示了"模块化"布局(modules/drivers/clickhouse)。该目录中三个关键文件值得展开说明:

deps.edn:声明驱动依赖

deps.edn指定驱动自身的依赖。以 modules/drivers/clickhouse/deps.edn 为例,它声明了:

{:paths ["src" "resources"] :deps {com.clickhouse/clickhouse-jdbc {:mvn/version "0.9.8" :exclusions [org.apache.commons/commons-lang3 org.lz4/lz4-java]} ;; pinned: clickhouse-jdbc pulls httpcore5-h2 5.3.4 org.apache.httpcomponents.core5/httpcore5-h2 {:mvn/version "5.4.3"} ;; pinned: clickhouse-jdbc 0.9.8 pulls httpclient5 5.4.4 org.apache.httpcomponents.client5/httpclient5 {:mvn/version "5.6.4"} ;; original org.lz4:lz4-java project is discontinued, this is the maintained fork at.yawk.lz4/lz4-java {:mvn/version "1.11.1"}}}

从中可以看到 JDBC 驱动的典型做法:引入对应的 JDBC 客户端依赖(此处为com.clickhouse/clickhouse-jdbc),并根据需要固定被间接引入的传递依赖版本。:paths ["src" "resources"]声明了源码目录与资源目录,后者即metabase-plugin.yaml所在位置。

resources/metabase-plugin.yaml:驱动清单

你的驱动的插件清单包含关于驱动的详细信息(名称、版本、父驱动、连接属性、初始化步骤等),Metabase 启动时遍历插件目录下每个 JAR 并读取这份清单。其完整语法将在下文专节展开。

src/metabase/driver/sqlite.clj:驱动核心文件

这是驱动的主文件。当前仓库中的 src/metabase/driver/sqlite.clj 是一个教科书级的例子:

(driver/register! :sqlite, :parent #{:sql-jdbc}) (defmethod driver/display-name :sqlite [_driver] "SQLite") (defmethod driver/connection-properties :sqlite [_driver] (into [] (mapcat u/one-or-many) [{:name "db" :display-name (tru "Filename") :placeholder "/path/to/toucan_sightings.sqlite" :required true} driver.common/advanced-options-start driver.common/default-advanced-options]))

关键点:

  • driver/register!注册驱动关键字:sqlite并声明父驱动:sql-jdbc
  • display-name返回管理界面展示的名称;
  • connection-properties返回连接表单需要用户填写的属性列表(此处是db文件名,必填),并追加公共的"高级选项"分节。

驱动文件内还通过doseq批量声明能力开关,例如:

(doseq [[feature supported?] {:right-join false :full-join false :regex false :percentile-aggregations false :schemas false :datetime-diff true :expression-literals true :now true ...}] (defmethod driver/database-supports? [:sqlite feature] [_driver _feature _db] supported?))

这对应 multimethoddatabase-supports?(src/metabase/driver.clj#L1037):SQLite 不支持的功能(如 right join、regex、schemas)显式置为false,避免在界面上暴露不可用的选项。

插件清单(metabase-plugin.yaml)详解

插件 JAR 的根目录包含一份名为metabase-plugin.yaml插件清单。Metabase 启动时会遍历 plugins 目录下每个 JAR,寻找其中的清单,据此得知插件提供了什么以及如何初始化它。以下官方示例完整保留:

info: name: Metabase SQLite Driver version: 1.0.0-SNAPSHOT-3.25.2 description: Allows Metabase to connect to SQLite databases. contact-info: name: Toucan McBird address: toucan.mcbird@example.com driver: name: sqlite display-name: SQLite lazy-load: true parent: sql-jdbc connection-properties: - name: db display-name: Filename placeholder: /home/camsaul/toucan_sightings.sqlite required: true init: - step: load-namespace namespace: metabase.driver.sqlite - step: register-jdbc-driver class: org.sqlite.JDBC

driver一节告诉 Metabase:插件定义了一个名为:sqlite、父驱动为:sql-jdbc的驱动。插件系统据此调用driver/register!,并利用display-nameconnection-properties自动为驱动生成对应 multimethod 的实现——这正是 src/metabase/driver.clj#L206 的 docstring 中所说"lazy-loaded driver 会在插件清单中声明、由lazy-loaded-driver自动创建实现"的机制。

懒加载

上例中驱动被标记为lazy-load: true:Metabase 启动时只创建方法实现,真正的初始化(加载命名空间、注册 JDBC 驱动等)推迟到第一次连接使用该驱动的数据库时才发生。你可以(但不应该)把驱动设为lazy-load: false,代价是 Metabase 启动更慢、占用更多内存。

初始化步骤(init)

Metabase 会按需自动初始化插件,流程为:把驱动加入 classpath,然后按顺序执行清单中每个init步骤。常见步骤有两种:

  • load-namespace:以 Clojure 标准require方式加载驱动命名空间(namespace: metabase.driver.sqlite)。如果你的驱动实现分散在多个命名空间,需要确保它们一并被加载——可以在主命名空间的:require中引用,也可以添加多个load-namespace步骤。
  • register-jdbc-driver:为基于 JDBC 的驱动注册底层 JDBC 驱动类(class: org.sqlite.JDBC)。

register-jdbc-driver 背后的原理

官方文档解释了register-jdbc-driver存在的深层原因:Java 的 JDBCDriverManager只使用由系统ClassLoader加载的 JDBC 驱动,而系统 classloader 不允许在运行时加载新的 classpath,Metabase 因此使用自定义ClassLoader初始化插件。为了解决这个限制,Metabase 内置了一个 JDBC 代理驱动类,可以包装其他 JDBC 驱动——调用register-jdbc-driver时,Metabase 实际注册的是该代理类的新实例,它把方法调用转发给真正的 JDBC 驱动,而DriverManager对此完全兼容。

依赖声明(dependencies)

清单可选地声明插件依赖,只有全部依赖满足时插件才会被初始化:

  • class依赖:检查某个类是否存在于 classpath(不初始化类,仅做可用性检查)。不要用它检查插件自身打包的类,只用于外部依赖。可附带message用于日志提示。
  • plugin依赖:检查某个插件是否可用,值为目标插件清单中的name,必须完全匹配。若依赖的插件尚未加载,Metabase 会在后续插件加载完成后重试——例如 BigQuery 驱动依赖共享的 Google 驱动,即使 BigQuery 先被尝试加载,等 Google 驱动就绪后 Metabase 也会检测到依赖满足并完成初始化。

完整带注释的清单参考

官方文档提供了一份带详细注释的完整清单,逐字段说明写法和默认值:

# 面向用户的基础信息放在 info: 下 info: name: Metabase SQLite Driver # 插件名称 version: 1.0.0-SNAPSHOT-3.25.2 # 建议遵循语义化版本;可在 patch 位附带主要依赖版本 description: Allows Metabase to connect to SQLite databases. dependencies: # 可选;全部满足才初始化插件 - class: oracle.jdbc.OracleDriver # 检查 classpath 中是否存在该类 message: > # 可选的提示信息(写入日志) Metabase requires the Oracle JDBC driver to connect to JDBC databases. - plugin: Metabase SQLHeavy Driver # 检查同名插件是否已加载 driver: # 插件定义的驱动 name: sqlite # 驱动关键字(如 :sqlite) display-name: SQLite # 管理员连接数据库时看到的名称 lazy-load: true # 默认 true;除非必要不要设为 false parent: sql-jdbc # 父驱动;也可用列表声明多父: # parent: # - google # - sql abstract: false # 是否抽象驱动,默认 false connection-properties: # 连接时向用户询问的属性 - dbname # 引用 metabase.driver.common 中的默认属性(按名引用) - host - name: db # 或使用完整 map 自定义属性 display-name: Filename placeholder: /home/camsaul/toucan_sightings.sqlite required: true - merge: # 用 merge: 合并多个 map,便于覆盖默认属性的细节 - port - placeholder: 1433 init: # 插件初始化步骤(懒加载驱动会延迟到首次连接) - step: load-namespace # require 一个 JAR 内的命名空间 namespace: metabase.driver.sqlite - step: register-jdbc-driver # 注册将被该驱动使用的 JDBC 驱动(实际注册代理驱动) class: org.sqlite.JDBC

关于connection-properties的更多细节,src/metabase/driver.clj#L618 的 docstring 指出:每个属性必须符合ConnectionDetailsPropertyschema(namedisplay-nameplaceholderrequired?options等可选键),并建议优先复用metabase.driver.common中预定义的公共属性(如default-host-detailsdefault-port-details)。

实现驱动 multimethod:驱动本质上只是一个关键字

实现 multimethod 让你得以复用 Metabase 现成的驱动代码,只针对你的数据库做差异化的扩展。以官方示例的 Visual Fox Pro '98 驱动为例,核心文件src/metabase/driver/foxpro98.clj的内容如下:

;; 为驱动定义命名空间 (ns com.mycompany.metabase.driver.foxpro98 (:require [metabase.driver :as driver])) ;; 实现 driver/display-name 这个 multimethod (defmethod driver/display-name :foxpro98 [_] "Visual FoxPro '98")

驱动命名空间规范

  • 每个 Metabase 驱动都位于独立的命名空间中。上述例子的命名空间是com.mycompany.metabase.driver.foxpro98;核心驱动统一位于metabase.driver.<驱动名>命名空间(如 src/metabase/driver/sqlite.clj 的metabase.driver.sqlite)。建议遵循 Java 包命名规范。
  • 较大型的驱动常拆出多个命名空间:常见的做法是独立的query-processor命名空间(如metabase.driver.foxpro98.query-processor)存放 MBQL → 原生查询的转换逻辑——查询处理器往往是驱动最复杂的部分,单独成文件更易维护;部分驱动还有独立的sync命名空间,实现数据库同步相关的方法。

驱动初始化

所有驱动都可以通过metabase.driver/initialize!挂载一段只执行一次的初始化代码,发生在驱动被初始化时(即首次连接数据库之前)。Metabase 正是借助initialize!实现驱动的懒加载。官方建议仅在确有需要时使用,例如分配资源或设置某些系统属性——注意 src/metabase/driver.clj#L204 中:default实现是一个 no-op。

metabase.driver 命名空间中的 multimethod

metabase.driver命名空间定义了一系列 multimethod,驱动通过defmethod为它们提供实现,并按驱动的关键字(上述例子中是:foxpro98)进行 dispatch。前文提到的四大核心职责全部由这些 multimethod 实现。事实上,一个 Metabase 驱动本质上就是一个关键字(keyword)——没有类、没有对象,只有一个关键字,加上针对该关键字的若干 multimethod 实现。metabase.driver中绝大多数方法都是可选的,阅读每个方法的 docstring 再决定是否需要实现。

列出可用的驱动 multimethod

快速查看所有驱动 multimethod 列表:

clojure -M:run driver-methods

该命令会打印所有驱动命名空间与 multimethod(包括sqlsql-jdbc的方法以及测试扩展方法)。若要连同 docstring 一起查看:

clojure -M:run driver-methods docs

父驱动:复用现成实现

很多驱动共享实现细节,若每个驱动都完整实现 sync 等方法会产生大量重复代码,因此大量高层功能已在共享的"父"驱动中部分或全部实现,其中最常用的父驱动是:sql-jdbc。父驱动可以类比面向对象编程中的超类(superclass)。在插件清单中列出父驱动即可声明父子关系。

几个重要的父驱动:

  • :sql-jdbc:适用于底层使用 JDBC 驱动的 SQL 数据库。它实现了大部分核心功能(例如driver/execute-prepared-statement!),但你需要实现metabase.driver.sql-jdbc.*命名空间中的sql-jdbcmultimethod,以及metabase.driver.sql.*命名空间中的部分方法。
  • :sql:sql-jdbc自身的父驱动,适用于没有JDBC 驱动的 SQL 数据库(如 BigQuery)。它实现了大量驱动功能,但使用它需要实现metabase.driver.sql.*中的一些方法。
  • 具体驱动作为父驱动:部分驱动以其他"具体"驱动为父,例如:redshift:postgres为父,只需在需要覆盖的地方提供实现。当前仓库中 SQLite 与 ClickHouse 均以:sql-jdbc为父(见 src/metabase/driver/sqlite.clj#L34 与 modules/drivers/clickhouse/src/metabase/driver/clickhouse.clj#L33)。
调用父驱动实现:get-method

可以用get-method获取父驱动对某方法的实现,等价于 OOP 中的super.someMethod()

(defmethod driver/mbql->native :bigquery [driver query] ((get-method driver/mbql-native :sql) driver query))

注意必须把 driver 参数原样传给父实现,否则父实现内部调用其他方法时会用错实现。以下是两种应避免的写法:

(defmethod driver/mbql->native :bigquery [_ query] ;; 错误!:sql 的 mbql->native 实现若调用其他方法,将不会使用 :bigquery 的实现 ((get-method driver/mbql->native :sql) :sql query))
(defmethod driver/mbql->native :bigquery [_ query] ;; 错误!若有人以 :bigquery 为父创建新驱动,:sql 实现内部调用的方法会用 :bigquery 的实现 ;; 而不是新驱动自己的实现 ((get-method driver/mbql->native :sql) :bigquery query))
多父驱动

BigQuery 同时以:sql:google为父,这种多继承是被允许且有帮助的。多父驱动可以通过driver/register!定义:

(driver/register! :bigquery, :parent #{:sql :google})

若两个父驱动对同一方法都有实现,解决歧义的办法是:为你的驱动提供自己的实现,并按上文方式转交给你偏好的父驱动实现。以插件形式发布的驱动则在插件清单中完成注册。

在 REPL 与 CIDER 中调试驱动

无需每次改动都重新构建 uberjar,可以像处理单个巨型项目一样直接启动 REPL:

clojure -A:dev:drivers:drivers-dev

但要注意:对驱动代码的修改仍需要重建驱动、安装到./plugins目录并重启 Metabase 才能生效。

构建与安装驱动插件

驱动的构建脚本说明见 bin/build-drivers.md。三个主要入口(均需要先安装 Clojure CLI 工具):

  • build-drivers:按需构建所有驱动。

    clojure -X:build:drivers:build/drivers # 或指定版本 clojure -X:build:drivers:build/drivers :edition :ee # 或使用 shell 包装脚本 ./bin/build-drivers.sh
  • build-driver:按需构建单个驱动(会先构建所需的父驱动)。

    clojure -X:build:drivers:build/driver :driver :sqlserver # 或 clojure -X:build:drivers:build/driver :driver :sqlserver :edition :oss # 或 ./bin/build-driver.sh redshift
  • verify-driver:验证构建出的驱动是否结构正确。

    clojure -X:build:build/verify-driver :driver :mongo

将构建得到的 JAR 放入 Metabase 的plugins目录(可通过MB_PLUGINS_DIR环境变量调整)即可被 Metabase 加载。

测试驱动:测试扩展(Test Extensions)机制

Metabase 内置了一套庞大的、会自动对所有驱动运行的共享测试套件,包括你的新驱动。要让自己的驱动通过这套测试,需要编写针对特殊测试扩展multimethod 的实现。测试扩展负责创建新数据库、为数据库定义(Database Definition)装载数据,并告诉 Metabase 可以从创建的数据库中期望到什么。

文件组织与命名约定

测试扩展通常放在metabase.test.data.<driver>命名空间中。以 SQLite 为例:

metabase/modules/drivers/sqlite/deps.edn ; <- deps 放在这里 metabase/modules/drivers/sqlite/resources/metabase-plugin.yaml ; <- 插件清单 metabase/modules/drivers/sqilte/src/metabase/driver/sqlite.clj ; <- 驱动主命名空间 metabase/modules/drivers/sqlite/test/metabase/test/data/sqlite.clj ; <- 测试扩展

测试扩展的接口定义在metabase.test.data.interface命名空间中。:sql:sql-jdbc自身实现了部分测试扩展,但定义了额外的你必须实现的方法(见metabase.test.data.sqlmetabase.test.data.sql-jdbc命名空间)。需要按如下别名 require 相关命名空间:

(require '[metabase.test.data.interface :as tx]) ; tx = test extensions (require '[metabase.test.data.sql :as sql.tx]) ; sql test extensions (require '[metabase.test.data.sql-jdbc :as sql-jdbc.tx])

注册测试扩展

与驱动本身一样,你需要声明驱动拥有测试扩展,避免 Metabase 重复加载。根据父驱动类型选择其一调用(只需调用一次):

# 非 SQL 驱动 (tx/add-test-extensions! :mongo) # 非 JDBC 的 SQL 驱动 (sql/add-test-extensions! :bigquery) # JDBC SQL 驱动 (sql-jdbc.tx/add-test-extensions! :mysql)

该调用应位于测试扩展命名空间的开头:

(ns metabase.test.data.mysql (:require [metabase.test.data.sql-jdbc :as sql-jdbc.tx])) (sql-jdbc.tx/register-test-extensions! :mysql)

当前仓库中 ClickHouse 的测试扩展 modules/drivers/clickhouse/test/metabase/test/data/clickhouse.clj#L29 正是以(sql-jdbc.tx/add-test-extensions! :clickhouse)完成注册的。

Metabase 测试的运行机制

以如下命令启动测试为例:

DRIVERS=mysql clojure -X:dev:drivers:drivers-dev:test

执行流程为:

  1. Metabase 检查:mysql的测试扩展是否已加载;若未加载,则(require 'metabase.test.data.mysql)

  2. 检查默认的test-data数据库是否已为 MySQL 创建、装载数据并同步;若未完成,调用测试扩展方法tx/load-data!创建test-data数据库并装载数据,随后同步测试数据库;

  3. Metabase 对 MySQLtest-data库的venues表执行 MBQL 查询。run-mbql-query宏是编写测试的辅助工具,$前缀符号会根据名称自动解析字段 ID。实际执行的查询形如:

    {:database 100 ; MySQL test-data 数据库的 ID :type :query :query {:source-table 20 ; 表 20 = MySQL test-data.venues :filter [:ends-with [:field-id 555] "Restaurant"] ; 字段 555 = venues.name :order-by [[:asc [:field-id 556]]]}} ; 字段 556 = venues.id
  4. 结果经过rowsformatted-venues-rows等辅助函数处理后,只保留关心的部分;

  5. 将实际结果与期望结果比对。

一个真实的测试片段如下:

;; expect-with-non-timeseries-dbs = 针对 DRIVERS 环境变量中列出的所有驱动运行(Druid 等时序数据库除外) (expect-with-non-timeseries-dbs ;; 期望结果 [[ 5 "Brite Spot Family Restaurant" 20 34.0778 -118.261 2] [ 7 "Don Day Korean Restaurant" 44 34.0689 -118.305 2] [17 "Ruen Pair Thai Restaurant" 71 34.1021 -118.306 2] [45 "Tu Lan Restaurant" 4 37.7821 -122.41 1] [55 "Dal Rae Restaurant" 67 33.983 -118.096 4]] ;; 实际结果 (-> (data/run-mbql-query venues {:filter [:ends-with $name "Restaurant"] :order-by [[:asc $id]]}) rows formatted-venues-rows))

装载数据:数据库定义(Database Definition)

为保证各驱动行为一致,Metabase 测试套件从一组共享的数据库定义创建新数据库并装载数据。也就是说,无论测试跑在 MySQL、Postgres、SQL Server 还是 MongoDB 上,同一个测试都能验证得到完全一致的结果。绝大多数数据库定义存放在 EDN 文件中,多数测试针对名为 "test data" 的测试数据库,其定义可在test/metabase/test/data/dataset_definitions/test-data.edn中找到——本质上就是一组表名、列名与类型,外加数千行待装载的数据。DatabaseDefinition的 schema 定义在metabase.test.data.interface中。

作为测试扩展的编写者,你最大的任务是:实现把数据库定义变成真实数据库(含表、列)并装载数据所需的方法。非 SQL 驱动需要实现tx/load-data!:sql:sql-jdbc为子驱动提供了共享实现,但定义了自己的测试扩展方法——例如:sql/:sql-jdbc负责建表的 DDL 语句,却需要你告诉它主键用什么类型,因此要实现sql.tx/pk-sql-type

(defmethod sql.tx/pk-sql-type :mysql [_] "INTEGER NOT NULL AUTO_INCREMENT")

同样,类型映射也需要按驱动定制,ClickHouse 测试扩展中的例子(modules/drivers/clickhouse/test/metabase/test/data/clickhouse.clj#L62):

(defmethod sql.tx/field-base-type->sql-type [:clickhouse :type/Boolean] [_ _] "Boolean") (defmethod sql.tx/field-base-type->sql-type [:clickhouse :type/Integer] [_ _] "Int32") (defmethod sql.tx/field-base-type->sql-type [:clickhouse :type/DateTime] [_ _] "DateTime64(3, 'GMT0')") (defmethod sql.tx/field-base-type->sql-type [:clickhouse :type/Float] [_ _] "Float64") (defmethod sql.tx/field-base-type->sql-type [:clickhouse :type/Text] [_ _] "String")

连接详情:dbdef->connection-details

Metabase 还需要知道如何连接你新建的数据库——具体而言,在把新建数据库保存为Database对象时,:detailsmap 中应写入什么。所有带测试扩展的驱动都必须实现tx/dbdef->connection-details,针对给定数据库定义返回合适的:details。MySQL 的官方示例:

(defmethod tx/dbdef->connection-details :mysql [_ context {:keys [database-name]}] (merge {:host (tx/db-test-env-var :mysql :host "localhost") :port (tx/db-test-env-var :mysql :port 3306) :user (tx/db-test-env-var :mysql :user "root") :serverTimezone "UTC"} (when-let [password (tx/db-test-env-var :mysql :password)] {:password password}) (when (= context :db) {:db database-name})))
连接上下文(context)

tx/dbdef->connection-details会在两种上下文中被调用:

  • 创建数据库时
  • 向数据库装载数据并同步时

大多数数据库不允许连接一个尚未创建的数据库(例如CREATE DATABASE "test-data";必须在指定test-data作为连接目标的情况下执行)。因此context参数取值为:

  • :server——"给我连接 DBMS 服务器(而非某个具体数据库)的连接信息";
  • :db——"给我连接某个具体数据库的连接信息"。

MySQL 的例子在context:db时追加:db连接属性。

ClickHouse 测试扩展中的真实实现(modules/drivers/clickhouse/test/metabase/test/data/clickhouse.clj#L77)展示了同样的模式,并在:db上下文下追加:db-filters-type/:db-filters-patterns等驱动特有参数。

从环境变量获取连接参数

测试几乎总是运行在本地 Docker 容器中,与其把用户名、主机、端口等连接细节硬编码,不如通过环境变量提供灵活性。tx/db-test-env-var用于从环境变量读取连接参数:

(tx/db-test-env-var :mysql :user "root")

这会让 Metabase 查找环境变量MB_MYSQL_TEST_USER,未找到时回退到默认值"root"。环境变量命名遵循MB_<driver>_TEST_<property>模式(前两个参数分别对应<driver><property>)。tx/db-test-env-var可以不指定默认值——如果该参数是可选参数且对应环境变量未设置,就不必出现在连接详情中。该函数的实现位于 test/metabase/test/data/interface.clj#L1111。

对于必须提供、又没有合理默认值的参数,使用tx/db-test-env-var-or-throw——对应的环境变量未设置时会抛出异常,最终导致测试失败:

;; 若 MB_SQLSERVER_TEST_USER 未设置,测试套件会以类似 ;; "MB_SQLSERVER_TEST_USER is required to run tests against :sqlserver" 的信息退出 (tx/db-test-env-var-or-throw :sqlserver :user)

注意:tx/dbdef->connection-details根本不会对未列入DRIVERS环境变量的驱动被调用,因此在跑 Mongo 测试时不会看到 SQL Server 的报错。db-test-env-var-or-throw的实现见 test/metabase/test/data/interface.clj#L1144。

除了tx/db-test-env-varmetabase.test.data.interface还有其他实用工具函数;如果数据库基于 SQL 可查阅metabase.test.data.sql,使用 JDBC 驱动可查阅metabase.test.data.sql-jdbc

其他测试扩展:命名差异与特殊 DBMS

比较测试结果时 Metabase 还需知道一些额外信息。例如不同数据库对表和列的命名方式不同——有些数据库会把全部名称大写(如venues变成VENUES),此时需要实现tx/format-name之类的方法,告诉 Metabase 这类命名差异仍视为同一对象。

对于不允许编程式创建数据库的 DBMS,常见解法是:用不同的schema代替不同的数据库,或给表名加数据库名前缀并在同一个数据库中创建。对 SQL 数据库,可以实现sql.tx/qualified-name-components让测试使用不同的标识符,例如用"shared_db"."test-data_venues".id代替"test-data".venues.id。SQL Server 与 Oracle 的测试扩展就是这类技巧的范例。ClickHouse 的实现也演示了这一点(modules/drivers/clickhouse/test/metabase/test/data/clickhouse.clj#L92)。

搭建 CI 运行驱动测试

所有测试通过后,需要在 GitHub Actions 中运行针对你的驱动的测试,即在.github/workflows/drivers.yml中新增一个 job。PostgreSQL 的官方配置示例:

be-tests-postgres-latest-ee: needs: files-changed if: github.event.pull_request.draft == false && needs.files-changed.outputs.backend_all == 'true' runs-on: ${{ vars.DEFAULT_RUNNER_KEY }} timeout-minutes: 40 env: CI: "true" DRIVERS: postgres MB_DB_TYPE: postgres MB_DB_PORT: 5432 MB_DB_HOST: localhost MB_DB_DBNAME: circle_test MB_DB_USER: circle_test MB_POSTGRESQL_TEST_USER: circle_test MB_POSTGRES_SSL_TEST_SSL: true MB_POSTGRES_SSL_TEST_SSL_MODE: verify-full MB_POSTGRES_SSL_TEST_SSL_ROOT_CERT_PATH: "test-resources/certificates/us-east-2-bundle.pem" services: postgres: image: circleci/postgres:latest ports: - "5432:5432" env: POSTGRES_USER: circle_test POSTGRES_DB: circle_test POSTGRES_HOST_AUTH_METHOD: trust steps: - uses: actions/checkout@v6 - name: Test Postgres driver (latest) uses: ./.github/actions/test-driver with: junit-name: "be-tests-postgres-latest-ee"

注意其中DRIVERS: postgres指定了要测试的驱动集合,而MB_POSTGRESQL_TEST_USER等环境变量正是上一节tx/db-test-env-var读取的MB_<driver>_TEST_<property>变量。驱动模块自带的docker-compose.yml(如 modules/drivers/clickhouse/docker-compose.yml,提供单节点、TLS、老版本与集群等多种测试拓扑)则用于在本地一键拉起数据库环境。

继续深入

本文对应的驱动开发完整路径为:

  1. 驱动基础(本文)
  2. 插件清单 plugins.md
  3. 实现驱动的 multimethod
  4. 为驱动提交 PR 与测试

动手前请先确认是否已有现成驱动可供贡献:官方支持的数据库与社区驱动列表,并参照开发环境搭建指南与 Clojure 开发指南完成准备工作。

【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询