2006年02月15日

在使用Hibernate的Lucene的时候,是通过事件监听器的方式来更新索引的。但是通常情况下,尤其是测试的时候,通过其他方式插入数据库,比如通过DbUnit。这个时候就需要同步的手动创建索引,我根据Hibernate Lucene的源代码,自己写了一个简单的程序。

原理很简单:先删除所有的索引,然后再添加。

我是对User这个实体进行索引的。

private static void remove(DocumentBuilder builder, Serializable id) {
  Term term = builder.getTerm( id );
  try {
   IndexReader reader = IndexReader.open( builder.getFile() );
   reader.delete( term );
   reader.close();
  }
  catch (IOException ioe) {
   ioe.printStackTrace();
  }
 }

 private static void add(final Object entity, final DocumentBuilder builder, final Serializable id) {
  Document doc = builder.getDocument( entity, id );
  try {
   File file = builder.getFile();
   IndexWriter writer = new IndexWriter( file, builder.getAnalyzer(), ! file.exists() );
   writer.addDocument( doc );
   writer.close();
  }
  catch (IOException ioe) {
   ioe.printStackTrace();
  }
 }

上面两个方法是直接拷贝了Hibernate Lucene 的LuceneEvent源代码中拷贝过来的。用来删除和添加索引。

protected static ApplicationContext ctx = null;

 static {
  String[] path = { "applicationContext-hibernate.xml };
  ctx = new FileSystemXmlApplicationContext(path);
 }

 /**
  * @param args
  */
 public static void main(String[] args) {
  UserManager userManager = null;
  userManager = (UserManager) ctx.getBean("userManager");
  List<User> users = userManager.getAllUsers();
  
  Analyzer analyzer = new CJKAnalyzer();
  String indexDirName = "indexes";
  File indexDir = indexDirName != null ? new File( indexDirName ) : new File( "." );
  final DocumentBuilder documentBuilder = new DocumentBuilder( User.class, analyzer, indexDir );
  for (int i = 0; i < users.size(); i++) {
   User user = (User) users.get(i);
   final Serializable id = user.getUsername();
   remove( documentBuilder, id );
   add( user, documentBuilder, id );
  }
 }

由于我是用Spring来集成Hibernate的,首先从Spring中获取UserManager 这个Bean,获得所有的用户,然后构建DocumentBuilder 对每个User实体构建索引即可。

2006年02月10日

在RDBMS里面做搜索是一件困难的事情。现在的需求是需要对用户的资料进行搜索。用户的资料由多个域组成,其中包含用户的爱好,工作经验等。这些域都是文本,包含大量的信息。

对用户这个实体使用标注@Indexed(index = "users")来指定本实体需要被索引,索引存放的文件夹是users,可以在Hibernate配置文件中使用hibernate.lucene.index_dir来指定默认的索引目录。

Lucene索引的4种类型,分别是Keyword,Text,Unstored,Unindexed。

为Hibernate添加监听器,这样当实体更新之后,索引也会自动更新。

<!– 插入Lucene事件监听器使得数据库更新时可以自动更新索引 –>
  <event type="post-commit-update">
   <listener class="org.hibernate.lucene.event.LuceneEventListener"/>
  </event>
  <event type="post-commit-insert">
   <listener class="org.hibernate.lucene.event.LuceneEventListener"/>
  </event>
  <event type="post-commit-delete">
   <listener class="org.hibernate.lucene.event.LuceneEventListener"/>
  </event>

如果有中文的话,使用下面的分析器:

<property name="hibernate.lucene.analyzer">org.apache.lucene.analysis.cjk.CJKAnalyzer</property>

这个分析器在Lucene官方网站的Sandbox里面有下载的。

需要注意的是,其中的Update监听器只有在显式调用update方法才能触发,也就是说使用saveOrUpdate是不会触发索引更新的。

完成索引之后,就可以用来搜索了。

IndexReader reader = IndexReader.open( new File( BASE_DIR, "users" ) );
   Searcher searcher = new IndexSearcher(reader);

  String keyword = "软件";
   String termStr1 = "address";
   String termStr2 = "jobExperience";
   Term term1 = new Term(termStr1, keyword);
   Term term2 = new Term(termStr2, keyword);

   BooleanQuery query = new BooleanQuery();
   Query query1 = new TermQuery(term1);
   Query query2 = new TermQuery(term2);
   query.add(query1, false, false);
   query.add(query2, false, false);

  Hits hits = searcher.search(query);
   System.out.println(hits.length() + " total matching documents");
   for (int i = 0; i < hits.length(); i++) {
    System.err.println(hits.doc(i));
   }

MySQL的中文乱码问题已经是个老问题,Google一搜就能搜到很多,我说下我的解决方法:

问题:

用Hibernate插入MySQL数据库之后,再取出来的是乱码。

解决:

修改数据库的连接URL,加上useUnicode=true&characterEncoding=UTF-8

使用UTF-8作为MySQL的默认字符集。

这样再用Hibernate取出来就是正常的了。

但是此时使用MySQL 的控制台查询数据的话,显示出来的还是乱码,需要在连接到数据库的时候使用:

mysql -u root -p –default-character-set=GBK

这样就可以看到正常的中文了。

2006年02月09日

同样的文章来源:http://dojotoolkit.org/docs/intro_to_dojo_io.html

下面论述的这些功能在IE,Mozila/Firefox上都可以,但是Safari不行。

可以在bind方法中添加对用户点击后退按钮的响应函数:

var sampleFormNode = document.getElementById("formToSubmit");

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.js",
    load: function(type, evaldObj){
        // hide the form when we hear back that it submitted successfully
        sampleFormNode.style.display = "none";
    },
    backButton: function(){
        // …and then when the user hits "back", re-show the form
        sampleFormNode.style.display = "";
    },
    formNode: sampleFormNode
});

上面的代码当提交之后表单隐藏,用户点击后退之后,重新显示表单。

如果用户再按了前进按钮,一样可以添加响应函数。

var sampleFormNode = document.getElementById("formToSubmit");

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.js",
    load: function(type, evaldObj){
        // hide the form when we hear back that it submitted successfully
        sampleFormNode.style.display = "none";
    },
    backButton: function(){
        // …and then when the user hits "back", re-show the form
        sampleFormNode.style.display = "";
    },
    forwardButton: function(){
        // and if they hit "forward" before making another request, this
        // happens:
        sampleFormNode.style.display = "none"; // we don’t re-submit
    },
    formNode: sampleFormNode
});

注意,前进按钮的触发器只有当用户在前进和后退按钮之间线性运动时才会触发。如果用户在后退两次之后,执执行了新的动作,那么新的bind请求会触发,下一个“前进”按钮不会触发了,因为生成了新的历史树,旧的已经无效了。

【原文】

Note that forward button triggers are only fired when the user is linearly progressing between the forward and back buttons. If the user takes an action after going back-back-forward that would fire a new bind() request, the next "forward" will have no callback fired from it since a new branch in the history tree has been created and the old one is likely invalid.


Dojo是一个Ajax的包,网址:http://dojotoolkit.org/

我看了上面的一片文章,是介绍Dojo.io的,就翻译了一下:网址:http://dojotoolkit.org/docs/intro_to_dojo_io.html

Dojo.io包封装了传输的细节问题,主要由Dojo.io.bind()方法来完成。

如果需要请求某个原始数据的话,代码如下:

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.txt",
    load: function(type, data, evt){ /*do something w/ the data */ },
    mimetype: "text/plain"
});

添加出错处理:

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.txt",
    load: function(type, data, evt){ /*do something w/ the data */ },
    error: function(type, error){ /*do something w/ the error*/ },
    mimetype: "text/plain"
});

可以注册一个单一的处理器来处理所有的情况:

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.txt",
    handle: function(type, data, evt){
        if(type == "load"){
            // do something with the data object
        }else if(type == "error"){
            // here, "data" is our error object
            // respond to the error here
        }else{
            // other types of events might get passed, handle them here
        }
    },
    mimetype: "text/plain"
});

请求JavaScript脚本并执行:

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.js",
    load: function(type, evaldObj){ /* do something */ },
    mimetype: "text/javascript"
});

需要确认是使用XMLHttp来传输的:

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.js",
    load: function(type, evaldObj){ /* do something */ },
    mimetype: "text/plain", // get plain text, don’t eval()
    transport: "XMLHTTPTransport"
});

提交表单:

dojo.io.bind({
    url: "http://foo.bar.com/processForm.cgi",
    load: function(type, evaldObj){ /* do something */ },
    formNode: document.getElementById("formToSubmit")
});

Dependent Mapping – Has one class perform the database mapping for a child class.
 
Dependent Mapping的原理是一个类(称为依赖者)依靠另外的类(称为所有者)来完成自己的数据库持久化的操作。每个依赖者有且仅有一个所有者。依赖者没有Identity Field,所以没有针对依赖者的查找方法,所有的查找操作都是由其所有者来执行的。这种依赖关系同样可以形成一个层次结构。对于一个对象来说,除了其所有者和依赖于该对象的对象之外,不应该有其它内存中的对象拥有对该对象的引用。在UML里面,依赖者和所有者之间的关系表示为组合。
 
对数据库的更新操作可以通过简单的全部删除的依赖者,然后重新插入新的全部的依赖者来实现。使用Dependent Mapping的话,要跟踪所有者对象的变化就更困难了。
 
使用Dependent Mapping的两个前提条件:
  1. 一个依赖者有且仅有一个所有者。
  2. 除了所有者和依赖自己的对象之外,没有其它对此对象的引用。

在实际中,避免较大的依赖关系的层次结构。当使用Dependent Mapping时不推荐使用Unit of Work,因为后者可以自动跟踪对象的变化情况。

Identity Field – Saves a database ID field in an object to maintain identity between an in-memory object and a database row.
 
Identity Field是很简单的。你所要做的只是把你的关系数据库表的主键存放在对象的域中即可。
使用Identity Field时需要考虑的几个问题:
  1. 如何选择主键。首先要考虑的是是否选择有意义的键。有意义的键由于人的失误可能会出错,不太可信。其次是使用简单键还是复合键。复合键通常是有意义的,注意保证其唯一性和不变规则。对键所进行的通常操作有相等测试和取得下一个键。因此需要考虑相等测试的性能和取得下一个键的容易程度。最后是选择表内唯一的键还是数据内唯一的键。使用数据库内唯一的键就可以使用单一的Identity Map。
  2. 如果在对象中表示Identity Field。最简单的表示形式是对象中和数据库表中主键类型一致的域。复合键比较复杂,最好写出一个键类(Key class)。
  3. 获得一个键。有3种方式:数据库自动生成:不推荐使用这种方式,不同的DBMS实现方式不同。GUID(全局唯一的标识符):一般过长,影响相等性测试的性能。自己生成主键:一种做法是用SQL的max函数来取得,这种做法严重影响并发的性能,不推荐。一个好的做法是单独使用一个键表(key table)。如果你使用表内唯一的主键,则键表中每一个行表示一个表的下一个可用的键。如果是数据库内唯一的主键,则键表中只有一列。使用键表的时候,把对表的访问放在一个单独的事务中,这样可以提高性能。使用键表的时候,如果使用表内唯一的主键,可以减少数据库表中行上的竞争。把获取新主键的代码放在一个单独的类中。
Active Record – An object that wraps a row in a database table or view, encapsultes the database access, and adds domain logic on that data.
 
Active Record包含了数据和行为。Active Record在本质上是Domain Model,只不过这个领域模型中的类和底层数据库的记录结构非常相似。Active Record的数据结构应该和数据库的紧密匹配:每个域对应数据库表中的一列。域的类型和数据库SQL提供的保持一致,并不需要在这个阶段进行类型转换。
 
Active Record类应该提供如下的方法:
  1. 从SQL的结果集中构造实例。
  2. 构造一个新的实例以供以后插入数据库使用。
  3. 包装了常用的SQL查询的静态查找方法,返回Active Record对象。
  4. 更新数据库和插入Active Record对象中的数据。
  5. 获取和设置域。(getter & setter)
  6. 实现某些领域逻辑。

Active Record适合于领域逻辑不太复杂的情况,简单的CRUD操作。Active Record很容易构建,也容易理解,不过问题在于只适合于Active Record对象和数据库表直接对应的情况;另外一个问题是它把对象设计和数据库设计耦合起来。

 

如果使用Transaction Script的话,Active Record是个很好的模式。可以从Gateway开始,然后逐步的重构,而得到Active Record。

Row Data Gateway – An object that acts as a Gateway to a single record in a data source. There is one instance per row.
 
Row Data Gateway是作为数据库中记录的代表的形式出现的,数据库中表的每行对应于该对象中相应的域。在设计时,通常对数据库中的每个表对应一个查找类,返回一个Gateway对象作为查找结果。
 
当使用Transaction Script的时候,一般使用Row Data Gateway;而使用Domain Model的时候通常不使用。Row Data Gateway可以和Data Mapper很好的结合起来使用。
An object that acts as a Gateway to a database table. One instance handles all the rows in the table.
 
一个Table Data GateWay封装了对一个数据库表或是视图的全部SQL访问:查询,插入,更新和删除。其它代码只是简单的调用它的代码来和数据库进行交互。
 
比如一个名为Person Gateway的类,它的方法可能有:
find(id):RecordSet
update(id, lastname, firstname)
insert(lastname, firstname)
delete(id)
 
Table Data GateWay的好处就是提供了一个简单的接口,而且通常是无状态的。一个需要考虑的问题是SQL查询返回的数据的表示问题。一种做法是返回简单的结构,比如Map;另一种做法就是使用Data Transfer Object来做。DTO比较好用。
 
如果你使用的是Domain Model的话,可以让Table Data GateWay返回合适的领域对象。
Table Data GateWay很适合于Transaction Script。
Table Data GateWay的另外一个好处就是对一般的SQL语句和存储过程提供同样的接口。