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
相关产品推荐
相关产品推荐

