You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jira插件开发:通过数据库动态填充下拉选择型自定义字段

我完全理解你现在的困境——开发Jira自定义Select字段时,想从数据库动态加载选项,结果不仅不知道从哪入手,手动填充还遇到了内部存储值不匹配的报错。其实这都是因为没对齐Jira原生的选项管理逻辑,下面给你一套完整的解决方案:

核心问题分析

你之前的报错本质是Jira的Select自定义字段要求选中值必须是其内部Option实体的ID,如果动态选项没有通过Jira官方API持久化,或者渲染时没有和Jira的存储逻辑对齐,就会出现值不匹配的问题。另外,getVelocityParameters只是处理模板参数的方法,频繁在这里创建选项会导致重复数据和性能问题,不是加载动态选项的正确时机。

完整解决方案

我们需要从数据库获取数据,通过Jira的OptionsManager正确同步选项,同时确保字段的读写逻辑和Jira原生行为一致。

1. 先写数据库查询工具类

首先封装一个从数据库获取选项的工具类,用Jira自带的连接池避免手动管理连接:

import com.atlassian.jira.component.ComponentAccessor;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

public class DbOptionFetcher {
    public static Map<Long, String> getDynamicOptionsFromDb() {
        Map<Long, String> optionsMap = new HashMap<>();
        // 使用Jira内置的数据库连接
        try (Connection conn = ComponentAccessor.getOfBizConnection().getConnection()) {
            // 替换成你的实际查询语句
            String sql = "SELECT id, option_name FROM your_custom_option_table";
            try (PreparedStatement stmt = conn.prepareStatement(sql);
                 ResultSet rs = stmt.executeQuery()) {
                while (rs.next()) {
                    Long id = rs.getLong("id");
                    String name = rs.getString("option_name");
                    optionsMap.put(id, name);
                }
            }
        } catch (SQLException e) {
            org.slf4j.LoggerFactory.getLogger(DbOptionFetcher.class)
                  .error("Failed to fetch dynamic options from database", e);
        }
        return optionsMap;
    }
}

2. 修改自定义字段类,同步动态选项

重写你的DynamicSelectCF类,加入选项同步逻辑,确保Jira内部的Options和数据库数据保持一致:

import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.issue.customfields.impl.SelectCFType;
import com.atlassian.jira.issue.customfields.manager.OptionsManager;
import com.atlassian.jira.issue.customfields.option.Option;
import com.atlassian.jira.issue.customfields.option.Options;
import com.atlassian.jira.issue.fields.config.FieldConfig;
import com.atlassian.jira.issue.fields.layout.field.FieldLayoutItem;
import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.issue.fields.CustomField;
import com.atlassian.jira.issue.search.SearchContextImpl;
import com.atlassian.plugin.spring.scanner.annotation.imports.JiraImport;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class DynamicSelectCF extends SelectCFType{
    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DynamicSelectCF.class);
    private final OptionsManager optionsManager;

    @Inject
    public DynamicSelectCF(@JiraImport CustomFieldValuePersister customFieldValuePersister, 
                           @JiraImport OptionsManager optionsManager, 
                           @JiraImport GenericConfigManager genericConfigManager, 
                           @JiraImport JiraBaseUrls jiraBaseUrls){
        super(customFieldValuePersister, optionsManager, genericConfigManager, jiraBaseUrls);
        this.optionsManager = optionsManager;
    }

    // 核心方法:同步数据库选项到Jira内部的Options
    private void syncDynamicOptions(FieldConfig fieldConfig) {
        Options existingOptions = optionsManager.getOptions(fieldConfig);
        Map<Long, String> dbOptions = DbOptionFetcher.getDynamicOptionsFromDb();
        Set<Long> dbOptionIds = dbOptions.keySet();

        // 1. 删除Jira中存在但数据库已移除的选项
        for (Option option : existingOptions) {
            if (!dbOptionIds.contains(option.getOptionId())) {
                optionsManager.deleteOption(option);
            }
        }

        // 2. 添加数据库新增的选项,同时更新名称有变化的选项
        for (Map.Entry<Long, String> dbEntry : dbOptions.entrySet()) {
            Option existingOpt = existingOptions.getOptionById(dbEntry.getKey());
            if (existingOpt == null) {
                // parent设为null表示根级选项,排序值用数据库ID保证顺序稳定
                optionsManager.createOption(fieldConfig, null, dbEntry.getKey(), dbEntry.getValue());
            } else if (!existingOpt.getValue().equals(dbEntry.getValue())) {
                optionsManager.updateOption(existingOpt, dbEntry.getValue());
            }
        }
    }

    @Override
    public Map<String, Object> getVelocityParameters(final Issue issue, final CustomField field, final FieldLayoutItem fieldLayoutItem) {
        final Map<String, Object> parameters = super.getVelocityParameters(issue, field, fieldLayoutItem);
        FieldConfig fieldConfiguration = issue == null 
                ? field.getReleventConfig(new SearchContextImpl()) 
                : field.getRelevantConfig(issue);

        // 先同步数据库选项到Jira
        syncDynamicOptions(fieldConfiguration);

        // 获取最新选项传给模板
        Options options = optionsManager.getOptions(fieldConfiguration);
        Map<Long, String> results = new HashMap<>();
        Long selectedId = -1L;

        if (issue != null) {
            Object value = field.getValue(issue);
            if (value instanceof Option) {
                selectedId = ((Option) value).getOptionId();
            }
        }

        for (Option option : options) {
            results.put(option.getOptionId(), option.getValue());
        }

        parameters.put("results", results);
        parameters.put("selectedId", selectedId);
        return parameters;
    }

    // 重写取值方法,兼容可能的旧数据
    @Override
    public Object getValueFromIssue(CustomField field, Issue issue) {
        Object storedValue = super.getValueFromIssue(field, issue);
        // 如果存储的是纯ID(比如之前手动测试的数据),转成合法的Option实体
        if (storedValue instanceof Long) {
            FieldConfig config = field.getRelevantConfig(issue);
            return optionsManager.getOptions(config).getOptionById((Long) storedValue);
        }
        return storedValue;
    }
}

3. 简化Velocity模板

你的edit.vm可以保持原结构,只需要确保类型匹配即可:

#* @vtlvariable name="results" type="java.util.Map" *#
#* @vtlvariable name="selectedId" type="java.lang.Long" *#
#controlHeader ($action $customField.id $customField.name $fieldLayoutItem.required $displayParameters.noHeader)
<select name="$customField.id" id="$customField.id" >
    <option value="-1">Not selected</option>
    #foreach ($mapEntry in $results.entrySet())
        #if ( $selectedId == $mapEntry.key )
            <option selected="selected" value="$mapEntry.key">$mapEntry.value</option>
        #else
            <option value="$mapEntry.key">$mapEntry.value</option>
        #end
    #end
</select>
#controlFooter ($action $fieldLayoutItem.fieldDescription $displayParameters.noHeader)
关键注意事项
  • 优化同步时机:如果数据库选项不会频繁变化,可以把同步逻辑放到字段初始化方法,或者用Jira的定时任务/事件监听(比如IssueCreateStartedEvent)来触发,避免每次页面加载都查数据库。
  • ID一致性:数据库里的选项ID要和Jira的OptionID保持一致,这是避免值不匹配的核心。
  • 清理旧数据:如果之前手动创建过测试选项,记得在Jira后台删除这些旧选项,避免和动态加载的选项冲突。

内容的提问来源于stack exchange,提问作者StarLord12345

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 23:13:09