ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

为新的数据库编写 Quartz.NET Driver Delegate:方言驱动委托完整指南

为新的数据库编写 Quartz.NET Driver Delegate:方言驱动委托完整指南 任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载本文以 Quartz.NET 官方 4.x 文档《A Driver Delegate for a New Database》为骨架完整讲解如何为 Quartz 内置六种数据库方言之外的新数据库编写驱动委托Driver Delegate。读完本文你将掌握何时应该继承StdAdoDelegate而非实现IDriverDelegate、十个方言契约成员的覆写时机与写法、行限制/分页/布尔映射/参数绑定四类 SQL 方言差异的处理方式以及如何把委托、DbMetadata提供程序注册和建表 DDL 三件套接入AddQuartz配置并让ProvisionSchema()自动建表。为什么需要一个新数据库的 Driver DelegateQuartz 的 ADO 持久化存储AdoJobStore通过一个驱动委托把调度语义翻译成具体数据库的 SQL。官方现成实现覆盖六种数据库方言分别位于仓库 src/Quartz/Impl/AdoJobStoreSQL ServerSqlServerDelegatePostgreSQLPostgreSQLDelegateMySQLMySQLDelegateOracleOracleDelegateSQLiteSQLiteDelegateFirebirdFirebirdDelegate当接入一种全新的数据库或者某种已在列表中的数据库但由行为不同的 Provider 提供时就需要一个自己的委托。起点不要实现 IDriverDelegate继承 StdAdoDelegate官方文档给出了明确的起点建议不要实现IDriverDelegate整个产品中没有任何东西实现它其约一百一十个成员中绝大多数在任何数据库上都是同一份 SQL。请继承StdAdoDelegate只覆写有差异的部分。六个内置方言合起来也只覆写了十个不同的成员其中 Firebird 和 PostgreSQL 各只需覆写两个。这个建议有源码支撑在 StdAdoDelegate.cs 中StdAdoDelegate声明为public partial class StdAdoDelegate : IDriverDelegate, IDbAccessor——公开、未密封且带有公共无参构造函数正是为作为可继承的方言基类而设计的。public sealed class MyDatabaseDelegate : StdAdoDelegate { // override only what differs }这段示例与下文所有 snippet 均可在仓库的 DialectDelegateSamples.cs 中直接找到该文件位于Quartz.Documentation.Samples项目它不是 Quartz 的友元程序集因此编译通过本身就证明这套公共 API 对任何外部开发者是完整可用的。接缝十个方言契约成员StdAdoDelegate的其他protected virtual成员应被视为私有实现步骤真正值得覆写的是下面这张契约表。这是方言委托的完整契约逐行继承自官方文档成员何时覆写protected virtual string? SchemaResourceName你希望ProvisionSchema()创建你的表——见下文还需要DbMetadata 与 schemaprotected virtual SqlRowLimit GetRowLimit(int count)你的数据库可以限制语句返回的行数protected virtual string GetSelectNextTriggerToAcquireSql(TriggerAcquisitionSqlShape shape)获取语句需要的不仅是行限制MySQL 的索引提示是唯一的内置案例protected virtual string GetSelectMisfiredTriggersToRecoverSql(int count)同上用于 misfire 扫描count -1表示不限制protected virtual string GetCountMisfiredTriggersInStateSql()计数形式的语句需要不同形态protected virtual string ApplyPaging(string sql, bool takeLimited)数据库不理解OFFSET … FETCH NEXT …protected virtual void AddPagingParameters(DbCommand cmd, int skip, int take, bool takeLimited)你的分页子句以不同顺序命名两个参数public virtual void AddCommandParameter(DbCommand cmd, string paramName, object? paramValue, Enum? dataType null, int? size null)Provider 需要显式设置类型或大小public virtual object GetDbBooleanValue(bool value)没有布尔列类型public virtual bool GetBooleanFromDbValue(object columnValue)同上读回时反向转换行限制Row limitingSQL 没有 ANSI 标准的行限制子句所以基类StdAdoDelegate默认不施加任何限制。触发器获取trigger acquisition和 misfire 扫描两条语句都会调用GetRowLimit。内置方言的覆写形态恰好覆盖了三种放置位置全部可以在仓库源码中逐一对证// … LIMIT n (PostgreSQL, MySQL, SQLite) — 或 Firebird 的 ROWS protected override SqlRowLimit GetRowLimit(int count) SqlRowLimit.AtStatementEnd(LIMIT, count); // SELECT TOP n … SqlRowLimit.InProjection(TOP, count) // SELECT * FROM ( … ) WHERE rownum n SqlRowLimit.InEnclosingSelect(rownum, count)逐条对应内置实现PostgreSQL / MySQL / SQLiteSqlRowLimit.AtStatementEnd(LIMIT, count)见 PostgreSQLDelegate.cs、MySQLDelegate.cs、SQLiteDelegate.csFirebirdSqlRowLimit.AtStatementEnd(ROWS, count)见 FirebirdDelegate.csSQL ServerSqlRowLimit.InProjection(TOP, count)见 SqlServerDelegate.csOracleSqlRowLimit.InEnclosingSelect(rownum, count)见 OracleDelegate.cs。SqlRowLimit本身是公开的readonly record struct定义在 SqlRowLimit.cs。它命名了行限制可以去的三个位置语句是带着它被构建出来的而不是事后对成品 SQL 做字符串拼接——在引入它之前SQL Server 方言曾经要靠裁掉前六个字符再贴回自己的SELECT这种脆弱方式实现TOP。使用要点count始终至少为 1表示取全部行的-1会在到达你的覆写之前被拦截并替换为SqlRowLimit.Unlimited见 StdAdoDelegate.Triggers.cs 与NoRowLimit -1哨兵值。TriggerAcquisitionSqlShape携带的是会改变语句文本的东西行限制以及NOT IN需要多少个 job 类型排除项。它同时是语句的缓存键因此它保存的是分桶后的排除项数量而不是精确值——缓存里只会沉淀少量条目见 StdAdoDelegate.cs 的acquisitionSqlByShape。只有当行限制表达不了的东西才需要覆写GetSelectNextTriggerToAcquireSql(shape)就像 MySQL 处理FORCE INDEX那样——仍然调用base并在其返回文本上做替换不要动行限制部分protected override string GetSelectNextTriggerToAcquireSql(TriggerAcquisitionSqlShape shape) base.GetSelectNextTriggerToAcquireSql(shape) .Replace({0}TRIGGERS t, {0}TRIGGERS t FORCE INDEX (IDX_{1}T_NFT_ST));MySQL 对 misfire 扫描的两条相关语句也做了同样的FORCE INDEX处理GetSelectMisfiredTriggersToRecoverSql与GetCountMisfiredTriggersInStateSql见 MySQLDelegate.cs让优化器强制走获取索引IDX_*_T_NFT_ST而非结构不匹配的 misfire 索引。分页Paging基类默认使用 ANSI 标准分页子句SQL Server 2012、Oracle 12c、PostgreSQL 和 Firebird 3 都能理解OFFSET pageSkip ROWS FETCH NEXT pageTake ROWS ONLYMySQL 和 SQLite 需要覆写ApplyPaging与AddPagingParameters它们的子句以相反的顺序命名两个参数而按位置绑定参数的 Provider 会按语句中的出现顺序绑定。参数名应使用AdoConstants.ParameterPageSkip与AdoConstants.ParameterPageTake在 SQL 文本中带前缀绑定时不带protected override string ApplyPaging(string sql, bool takeLimited) takeLimited ? sql LIMIT AdoConstants.ParameterPageTake OFFSET AdoConstants.ParameterPageSkip : sql LIMIT -1 OFFSET AdoConstants.ParameterPageSkip; protected override void AddPagingParameters(DbCommand cmd, int skip, int take, bool takeLimited) { if (takeLimited) { AddCommandParameter(cmd, AdoConstants.ParameterPageTake, take); } AddCommandParameter(cmd, AdoConstants.ParameterPageSkip, skip); }细节要点takeLimited为false表示无界分页Take int.MaxValue。因为没有仅 OFFSET的写法MySQL 使用最大的BIGINT UNSIGNED源码中是字面量LIMIT 18446744073709551615 OFFSET …见 MySQLDelegate.csSQLite 使用LIMIT -1。基类传入的take是页面大小加一多取的那一行让PagedResultT.HasMore判断精确成立。布尔值BooleansOracle 没有布尔列类型所以它的委托双向映射1/0见 OracleDelegate.cspublic override object GetDbBooleanValue(bool booleanValue) booleanValue ? 1 : 0; public override bool GetBooleanFromDbValue(object columnValue) Convert.ToInt32(columnValue) 1;IS_DURABLE、REQUESTS_RECOVERY等列都通过GetDbBooleanValue绑定参数因此写入与读回两个方向的映射必须互相一致。基类默认实现直接把bool原样交给 Provider并在读回时用Convert.ToBoolean转换见 StdAdoDelegate.cs。参数ParametersAddCommandParameter是最后的兜底手段。以 SQL Server 为例见 SqlServerDelegate.cs它做了三件事把bool参数转换为1/0照顾 SQL Server CE对二进制参数dataType匹配DbProvider.Metadata.BinaryParameterType设置size -1即 varbinary(MAX)对字符串参数在没有显式size时固定为size 4000——避免 SQL Server 为每种字符串长度各建一份查询计划。覆写后务必调用base.AddCommandParameter(...)完成实际的参数添加。委托无法触及的部分::: warningStdAdoConstantsSQL 文本是internal的。Schema 才是契约它公开在AdoConstants中。 :::你不能写StdAdoConstants.SqlSelectNextTriggerToAcquire。要么转换base.GetSelectNextTriggerToAcquireSql(shape)的返回值像 MySQL 的.Replace({0}TRIGGERS t, …)那样要么完整地自己写出整条语句。你可以直接引用公开的表名、列名、状态常量AdoConstants.TableTriggers、AdoConstants.ColumnTriggerName、AdoConstants.StateWaiting等。{0}是表前缀占位符protected string ReplaceTablePrefix(string query)负责替换它。基类返回的语句文本中仍包含{0}由调用方在真正执行时完成替换。另一条重要限制官方文档明确提示只定制某一条语句是不被支持的。六个 SQL 钩子覆盖了数据库之间的差异其余约 76 条语句都是内联的ReplaceTablePrefix(StdAdoConstants.X)调用。请改为覆写发出该语句的方法StdAdoDelegate上的每一个IDriverDelegate成员都是virtual的包括无锁获取路径用来认领触发器的UpdateTriggerStateFromOtherStateWithNextFireTime。未来也可以兼容地增加更多GetXxxSql()钩子如果确实需要可以向项目提出。值得留意的是这些整体重写成员的写法在文档示例中也有体现——DialectDelegateSamples.cs中展示了一个完整的SelectTriggerState覆写它用ReplaceTablePrefix组合公开常量拼出 SQL、用PrepareCommand准备命令、用AddCommandParameter绑定参数、用SchedulerName限定作用域再通过StoredTriggerStates.FromStoredValue把字符串映射回状态。这说明只要钩子不够用整个成员写出来是受支持且所有原材料都可达的。值转换并不全是接缝GetDbBooleanValue/GetBooleanFromDbValue是virtual的但下面这四个不是GetDbDateTimeValue、GetDateTimeFromDbValueGetDbTimeSpanValue、GetTimeSpanFromDbValue原因源码有明确注释见 StdAdoDelegate.csUTC 时钟周期ticks和整毫秒是 schema 契约的一部分。首选节点preferred-node的存活探测 SQL 直接对LAST_CHECKIN_TIME和CHECKIN_INTERVAL做 tick 算术如果某个委托改了这两种值的存储格式会让固定触发器pinned trigger的故障转移在无任何报错的情况下静默失效。因此官方文档给出结论必须原生存储DATETIME的数据库应当直接实现IDriverDelegate并完全拥有自己的 SQL。初始化DriverDelegateContextStdAdoDelegate.Initialize(DriverDelegateContext context)是public virtual六个内置方言都没有覆写它。默认实现从上下文拷贝tablePrefix、schedulerName、instanceId、DbProvider、TypeLoader、useProperties、ObjectSerializer、TimeProvider创建AdoUtil注册五个内置触发器族Simple、Cron、CalendarInterval、DailyTimeInterval、Recurrence的持久化委托再追加容器传入的自定义持久化委托见 StdAdoDelegate.cs。只在需要注册额外的触发器持久化委托或从上下文中捕获某些值时才覆写它并且必须先调用base.Initialize(context)。DriverDelegateContext成员一览成员说明TablePrefix、SchedulerName、InstanceId必填DbProvider、TypeLoader必填ObjectSerializer可空TriggerPersistenceDelegates为本调度器注册的那些TimeProvider调度器的时钟CommandTimeout来自AdoJobStoreOptions.CommandTimeout为什么上下文在构造之后才送达因为当容器构建委托时自动生成的InstanceId还不存在。基类暴露了protected string SchedulerName { get; }几乎每条语句都按它做作用域限定和protected IDbProvider DbProvider { get; }见 StdAdoDelegate.cs所以你的子类不需要自己再保存一份副本。注册委托builder.Services.AddQuartz(q { q.UsePersistentStore(s { s.UseDriverDelegateMyDatabaseDelegate(); s.UseGenericDatabase(MyProvider, connectionString); }); });::: warning 顺序很重要 注册是先到先得TryAdd语义。UseSqlServer、UsePostgres等在内部都会调用UseDriverDelegate…()所以UseDriverDelegateMyDatabaseDelegate()必须放在数据库方法之前否则会被静默忽略。 :::这与 PersistentStoreBuilder.cs 的实现一致UseDriverDelegateT()与UseDriverDelegate(FuncIServiceProvider, IDriverDelegate)最终都走TryAdd系的注册逻辑。其他要点委托由ActivatorUtilities构建所以构造函数依赖注入可用例如ILoggerMyDatabaseDelegate或容器中的任何东西。UseDriverDelegate(factory)接受一个由你构建委托的工厂。工厂的 Provider 解析的是该调度器自己的部件注册在全局Services上的IDriverDelegate对命名调度器不可见——命名调度器在它自己的键key下解析。UseSerializer、UseLockHandler、UseConnectionProvider、UseTriggerPersistenceDelegate都以同样的方式接受工厂。旧式配置键quartz.jobStore.driverDelegateType仍然按类型名选择委托即使存储本身是代码配置的也依然生效。还需要DbMetadata 与 schema官方文档明确新数据库三件套委托——本文主题Provider 注册。UseGenericDatabase(provider, connectionString, describeMetadata)接受一个FuncDbMetadata来描述 ADO.NET Provider连接、命令和参数类型、参数前缀、DbType拼写。Provider 与委托相互独立Use…快捷方法是一次性把两者都设置好。DDL。复制最接近的database/tables/tables_ .sql并调整列类型。仓库的 database/tables 目录下有六种方言的建表脚本tables_sqlServer.sql、tables_postgres.sql、tables_mysql_innodb.sql、tables_oracle.sql、tables_sqlite.sql、tables_firebird.sql。手工执行 DDL 即可满足默认的SchemaProvisioning.Validate。若要让ProvisionSchema()自动建表需要三步嵌入脚本EmbeddedResource IncludeMyDatabase.sql /从SchemaResourceName返回其清单资源名RootNamespace.文件夹路径.文件名语句之间用内容为--;;的行分隔因为存储过程体内可能含分号并在脚本中写{0}作为表前缀占位符。// 该委托据以建表的 schema 脚本与它并排嵌入 // EmbeddedResource IncludeMyDatabase.sql /名称与编译器命名方式一致 // RootNamespace.文件夹路径.文件名。语句之间以 --;; 行分隔。 protected override string? SchemaResourceName MyCompany.Quartz.MyDatabase.sql;这个名字会先在委托类型所在的程序集中查找再沿基类链向上查找——因此内置方言的子类会直接继承该方言的脚本内置方言的SchemaResourceName如Quartz.Impl.AdoJobStore.Schema.create_mysql_innodb.sql见 MySQLDelegate.cs。若未设置ProvisionSchema()会以一条点名该成员的错误信息失败。小结一份新方言的落地清单把上面的内容收敛成实操顺序继承StdAdoDelegate对照十成员契约表只覆写你的数据库真正有差异的部分多数情况下行限制 分页即可提供FuncDbMetadata描述你的 ADO.NET Provider并用UseDriverDelegateMyDelegate()先于数据库方法UseGenericDatabase注册基于 database/tables 中最接近的脚本编写 DDL可选地嵌入资源并实现SchemaResourceName以启用自动建表别忘了为布尔、日期时间等值转换确认双向一致且不要触碰 UTC ticks/整毫秒这两条 schema 契约。参见Job Stores —— ADO 存储是如何组装起来的A Job Store of Your Own —— 比本页高一层Persisting a Custom Trigger Type —— 另一个委托接缝赞分享任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载相关推荐面向 Antigravity 的委托任务简报Brief写作实战基于 agentic-awesome-skills 的 agy-delegate 完整指南面向 Antigravity 的委托任务简报Brief写作实战基于 agentic awesome skills 的 agy delegate 完整指南AI 技能AI 插件dataset测试驱动开发为SQL数据库应用编写单元测试的完整指南dataset测试驱动开发为SQL数据库应用编写单元测试的完整指南 在Python数据开发领域dataset库以其简洁的API和强大的功能赢得了众多开发者的数据库后端开发工具Lombok Delegate注解委托模式的终极自动化实现指南Lombok Delegate注解委托模式的终极自动化实现指南 Lombok的Delegate注解是Java开发中实现委托模式的终极自动化解决方案。作为L开发工具代码生成上一篇dotnet-bluetooth-le 项目使用教程下一篇如何快速构建美观Electron应用Photon开源项目完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表