Jive源码分析:tree树形数据结构

板桥里人 https://www.jdon.com 2002/06/10

Jive是著名的open source论坛,这次我们来研究其帖子结构,论坛是由一个个帖子组成,一个帖子后跟一个或几个回帖,这是一个典型的树形结构,有枝有叶,如下:

1
|-- 3
|-- |--4
|-- |--6
|-- 5

树形结构的应用是非常广泛的,如目录分类系统,菜单系统等等,所以,理解了Jive的应用原理,我们就可以在我们自己的系统中灵活对付树形结构.

这里主要是谈使用关系数据库如Mysql存放树形结构数据,然后我们再在内存中将其展开;以后我们尝试使用XML来对树形结构数据的存储.

Jive中有三个基本对象:
1.Forum :论坛Forum 数据库中对应有forumID;
2.message:帖子,数据库中对应有messageID;
3.Thread:Thread是代表一系列Message的层次结构.可以理解成是messages的目录,是"枝";那么帖子messages当然就是"叶";"枝"和"叶"的区别就是:"枝"下面还可以有"枝"或"叶";而"叶"就是单独元素了.典型的树形数据结构出来了.

在外部,Thread和Messages一样,是作为一个独立的Object存在,可以象创建查询帖子一样创建查询它,因此数据库中对应有ThreadID。

在Thread内部,有两种办法访问它里面的messages:

1.TreeWalker 提供了一种树形结构的视图,这是我们下面要详细讨论的;
2.Iterator 这是一个扁平形的视图。

我们知道,关系数据库是不擅长存放树形结构数据的,因此,一旦使用关系数据库存放树形结构数据,以后访问查询时,免不了有N多SQL查询语句,降低了系统速度;一般最原始的解决办法是在代号上做文章,代号上分几个部分,每个部分用几位数,这种办法局限性显然很大,(可惜写这篇文章时发现就有 易趣 等著名网站使用这种办法。)

而Jive中是使用TreeWalker是将数据库中的数据在内存中建立起树形结构,同时又实现了缓冲,从而大大加快了数据的访问速度。

这里关键是它的一个util类:LongTree 就是这个类提供了对数据的树形结构建立 修改访问等功能:

public final class LongTree implements Cacheable {

//使用这三个数组存放数据 已经数据关系
//我们把要我们自己的数据ID放入数组keys中,leftChildren和rightSiblings是记录数组keys的数组ID之间关系
//leftChildren放置的是儿子 rightSiblings存放的是其兄弟(并列的〕    
long [] keys;
//char arrays let us address get about 65K nodes.
char [] leftChildren;
char [] rightSiblings;

// Pointer to next available slot.
char nextIndex = 2;

/**
* 构造一个新的Tree只要提供两个参数:根数据ID 和 Tree结构的大小
*/
public LongTree(long rootKey, int size) {
  keys = new long[size+1];
  leftChildren = new char[size+1];
  rightSiblings = new char[size+1];

  //新Tree,第一个元素当然存放我们提供的参数:根数据ID :rootKey
  // New tree, so set the fields to null at root.
  keys[1] = rootKey;
  leftChildren[1] = 0;
  rightSiblings[1] = 0;
}

/**
* Adds a child to the tree.
*
* @param parentKey 将新的儿子加入哪个父亲下面,所以需要提供父亲的数据ID
* @param newKey 新的儿子本身的数据ID
*/
public void addChild(long parentKey, long newKey) {
  // Find record for parent 检查提供的父亲是否存在
  char parentIndex = findKey(parentKey, (char)1);
  if (parentIndex == 0) {
    throw new IllegalArgumentException("Parent key " + parentKey +
    " not found when adding child " + newKey + ".");
  }

  // Create record for new key.加入数组 前后关系暂时设定为0
  keys[nextIndex] = newKey;
  leftChildren[nextIndex] = 0;
  rightSiblings[nextIndex] = 0;

  // Adjust references. Check to see if the parent has any children.
  if (leftChildren[parentIndex] == 0) {
    // No children, therefore make the new key the first child.
    leftChildren[parentIndex] = nextIndex;
  }
  else {
    // The parent has children, so find the right-most child.
    //如果这个父亲有儿子了,那么找出其最右边的儿子,类似上图的 6
    //这样是为了将新的儿子追加在其后面,或者说最最右边
    long siblingIndex = leftChildren[parentIndex];
    while (rightSiblings[new Long(siblingIndex).intValue()] != 0) {
      siblingIndex = rightSiblings[new Long(siblingIndex).intValue()];
    }
    // Add the new entry as a sibling of that last child.
    rightSiblings[new Long(siblingIndex).intValue()] = nextIndex;
    }

    // Finally, increment nextIndex so it's ready for next add.
    nextIndex++;
}

...........

}


从上面简单的分析LongTree的加入儿子的方法,我们大概理解这个类的原理和使用方法,让我们来看看TreeWalker是怎么使用LongTree的。

TreeWalker有个子类DbTreeWalker:

我们先看看存放树形结构数据的数据库:

CREATE TABLE jiveMessage (
  messageID BIGINT NOT NULL,
  parentMessageID BIGINT NULL, #记录当前帖子的父亲
  threadID BIGINT NOT NULL, #当前帖子的threadID 就是目录ID
  forumID BIGINT NOT NULL,
  userID BIGINT NULL,
  subject VARCHAR(255),
  body TEXT,

....

)

我们看到其数据库主要用两个字段来指明这种树形结构数据的之间的关系。

再看看DbTreeWalker是怎么使用LongTree把存放在这个数据库中数据展现开来的。

public class DbTreeWalker implements TreeWalker {

/** DATABASE QUERIES SQL 语句**/
private static final String GET_MESSAGES =
"SELECT messageID, parentMessageID, creationDate FROM jiveMessage " +
"WHERE threadID=? AND parentMessageID IS NOT NULL " +
"ORDER BY creationDate ASC";

........

private long threadID;
private DbForumFactory factory;
//这里设置使用LongTree准备生成一个对象
private LongTree tree;

/**
* TreeWalker 构造函数
*/
public DbTreeWalker(DbForumThread thread, DbForumFactory factory) {
  this.threadID = thread.getID();
  this.factory = factory;

  ForumMessage root = thread.getRootMessage();
  int numMessages = thread.getMessageCount(IGNORE_MODERATION_FILTER);

  // Create the tree, set the root.创建一个新的Tree
  tree = new LongTree(root.getID(), numMessages);

  // Now, build the rest of the tree.建立完整这个树
  //到数据库中,将当前目录下 (当前Thread)所有数据装入这个Tree
  Connection con = null;
  PreparedStatement pstmt = null;
  try {
    con = ConnectionManager.getConnection();
    pstmt = con.prepareStatement(GET_MESSAGES);
    pstmt.setLong(1, thread.getID());
    ResultSet rs = pstmt.executeQuery();
    while(rs.next()) {
      long messageID = rs.getLong(1);
      long parentMessageID = rs.getLong(2);
      //加入儿子
      tree.addChild(parentMessageID, messageID);
    }
  }

  。。。。。
  }
}

.......

//可以很方便的得到一个帖子的父亲
public ForumMessage getParent(ForumMessage message)
throws ForumMessageNotFoundException
{
........
}

//可以很方便的得到一个帖子的儿子
public ForumMessage getChild(ForumMessage message, int index)
throws ForumMessageNotFoundException
{
...........
}

Jive中使用Cache技术,保证了经常访问的那些Tree能够放置在内存中,这是另外篇幅需要研究的。

总之,我们找到了一套树形结构数据+关系数据库+面向对象语言三者结合的一种办法,这种办法无论从理论上 以及实用性上,都是值得我们在实践中借鉴的。

Jive专题

Composite模式和树形结构的讨论

更多Composite模式讨论

其他树形结构讨论