用户自定义字段

2013 年 7 月 23 日

软件系统中一个常见的特性是允许用户在数据结构中定义自己的字段。考虑一个地址簿——你可能想要添加很多东西。随着每天都有新的社交网络出现,用户可能希望为他们的联系人添加一个新的 Bunglr ID 字段。

出于内存目的,通常最好的方法是允许类包含一个用于用户定义字段的哈希映射字段(肯特·贝克称之为可变状态)。

# ruby
class Contact
  attr_accessor :firstname, :lastname

  def initialize
    @data = {}
  end

  def [] arg
    return @data[arg]
  end
  def []= key, value
    @data[key] = value
  end
end

aCustomer = Contact.new
aCustomer.firstname = "Martin"
aCustomer[:bunglrId] = 'fowl'

使用这样的设置,你可以向你的 UI 添加功能,以允许用户将新字段附加到对象。如果你想要常见的用户定义字段,你可以使用一个类变量来保存哈希映射的常用键列表。常规字段和用户定义字段的访问方式不同,这会有一些尴尬,但根据你的语言,即使这也可以克服。如果你的语言支持动态接收,那么你可以使用它以常规字段访问的方式访问哈希映射。

class Contact...

  def method_missing(meth, *args)
    if @data.has_key? meth
      return @data[meth]
    else
      super
    end
  end

通常,最棘手的部分是弄清楚如何持久化它。如果你使用的是无模式数据库,那么通常很简单——你只需将用户定义的键添加到你的应用程序定义的键中。棘手之处在于具有存储模式的数据库,尤其是关系数据库。

通常最好的选择是使用序列化 LOB,本质上是创建一个大型文本列,你可以在其中将用户定义的字段存储为 JSON 或 XML 文档。如今,许多数据库都为这种方法提供了相当不错的支持,包括对基于 LOB 内数据结构的索引和查询的支持。但是,这种支持(如果可用)通常比使用字段更麻烦。 [1]

另一种方法是使用某种属性表。一个表可能看起来像这样。

CREATE TABLE ContactAttributes (
  contactId   INTEGER, 
  attribute   TEXT, 
  value       TEXT, 
  PRIMARY KEY (contactId, attribute))

同样,查询和索引很麻烦。查询可能涉及大量的额外联接,这会变得相当混乱。

预定义的自定义字段提供了另一种系统。在这里,你使用诸如custom_field_1(以及可能custom_field_1_name)之类的字段来设置模式。你仅限于你预先定义的每个实例的自定义字段数量。像往常一样,索引和查询很麻烦。

使用属性表或预定义的自定义字段时,你可能选择为不同的 SQL 数据类型使用不同的列。因此,预定义的字段可能是integer_1, integer_2, text_1…,或者属性表可能具有多个值字段(text_value, integer_value)。

动态模式是一种经常被忽视的方法。要做到这一点,你将事情设置好,以便当有人添加字段时,你使用alter table语句将该字段添加到表中。我们的 Mingle 团队就是这样做的,并且对它的效果感到满意。 [2] 新字段可以像应用程序定义的字段一样被索引和查询。但这意味着所有实例都将获得所有字段,因此如果你在实例之间存在大量差异,则不太方便。

你的持久化方案选择将受到你用于关系映射的内容的影响。用户定义的字段不是关系映射问题中最常被讨论的部分,因此不同关系映射库的支持存在很大差异。

用户定义的字段与非统一类型[3]类似。这两个问题都导致需要更灵活的模式,或者实际上是真正无模式的方法(尽管请记住无模式并不意味着你没有模式)。如果你有非统一类型,这些类型不会在用户的要求下更改,那么其中一种面向继承的模式可能是有意义的。(单表继承类表继承具体表继承。)

备注

1: Bret Taylor 描述了一种方案,通过为每个可索引字段构建单独的索引表来索引此类方案中的字段。

2: Mingle 的方法实际上比仅仅将字段添加到现有表中更复杂。Mingle 的核心记录类型是卡片(代表故事、任务等)。卡片上的字段因项目而异,你可以在同一个数据库中拥有多个项目。因此,Mingle 并没有使用单个卡片表,而是为每个项目的卡片创建了一个新表。然后,它根据用户的需要动态地向该表添加字段。

3: 非统一类型是指实例使用少量且截然不同的字段选择。有时它们被称为稀疏表,因为如果你查看整个表,每行仅使用大量列中的少量列。非统一类型和用户定义字段之间的区别在于,非统一类型的所有潜在字段都是开发人员已知的,而用户定义的字段允许创建开发人员永远不会知道的字段。