ActiveRecord destroy之后的事情

一般的Rails应用都在对象destroy之后自动跳转到另一个页面,不再去关心被destroy的对象如何了。其实被destroy的对象虽然从数据库中被删除了,但仍然存在于内存当中。

举个例子吧,比如我们做个博客系统,有文章,有评论,当我们删除一个日志的时候,需要在日志中做下记录

class Post
  after_destroy :log

  def log
    Logger.create(:action => 'destroy', :object_type => self.class, :object_id => self.id, :object_value => self.title)
  end
end

可见,我们是在post被删除之后再做日志记录,此时我们仍然能够得到post对象,并成功记录到日志系统中去。

如果再加些BT的需求呢,要求在日志系统中同时记录子对象(即comments对象)的type, id和value。看看很简单,但是你会不会想到,comments在post之前就被删除了,我们去哪里拿这些数据呢?答案就是内存中

class Post
  has_many :comments, :dependent => :destroy
  after_destroy :log

  def log
    Logger.create(:action => 'destroy', :object_type => self.class, :object_id => self.id, :object_value => self.title, :associations_value => comments.collect(&:name).join(','))
  end
end

在comments和post相继被删除之后,我们仍然可以得到post的name和post所有commnets的name,为什么呢?

首先看看:dependennt => :destroy有什么作用

method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
define_method(method_name) do
  send(reflection.name).each { |o| o.destroy }
end
before_destroy method_name

也就是说在destroy post之前,会遍历所有的comments,然后逐一删除comment。

由于ActiveRecord::AssociationProxy的作用,当遍历comments之前会调用load_target来读取所有的comments

def load_target
  if !@owner.new_record? || foreign_key_present
    begin
      if !loaded?
        if @target.is_a?(Array) && @target.any?
          @target = find_target + @target.find_all {|t| t.new_record? }
        else
          @target = find_target
        end
      end
    rescue ActiveRecord::RecordNotFound
      reset
    end
  end

  loaded if target
  target
end

也就是说,在删除所有的comments之前,ActiveRecord已经将post所有的comments读取过来,并赋值给@target,所以我们才能在post.destroy之后读取到post.comments的name

Posted in  rails activerecord


github最近有点不稳定

今天早上在github上面发布bullet插件的第一个gem包,结果等了半天没结果,一会返回Queued for rebuild,一会什么都不返回。在support上面发帖提问,被告知可能是gem build进程运行在low priority,他们会把它升到medium priority。

等到下午快3点才收到通知说gem已经build成功,不过到现在还没有被加到github的gem list上,说是晚上会强制做一次reindex,希望那时候会成功gem install。

Posted in  life


ruby数字的科学记数法显示

当网页需要显示很长的数字时,比如:100000000, 0.00000001,有时候会影响页面布局,而且也不方便阅读。改用科学记数法就会方便很多,比如:1e+08, 1e-08。ruby和其它语言一样,可以通过String的format来格式化数字。

def number_to_scientific(num)
  "%g" % num
end

>> number_to_scientific(100000000)
=> "1e+08"
>> number_to_scientific(0.000000001)
=> "1e-09"

Posted in  ruby


contact-list类库依赖包之msnmlib

msnmlib是韩国人写的一个msn的java客户端,提供了完整的api,很好用的,唯一的缺陷就是javadoc使用韩文写的,看不懂。

对于contact-list类库来说,完全是基于msnmlib来提供对msn联系人列表的导入。

  1. 登录msn
private void login() {
    msn.setInitialStatus(UserStatus.OFFLINE);
    msn.login(username, password);
}

设置初始登录状态为OFFLINE,可以防止被误以为是用户登录,影响用户之间的联系。

  1. 获取联系人列表
public List<Contact> getContacts() throws ContactsException {
    try {
        login();
        List<Contact> contacts = new ArrayList<Contact>();
        BuddyList list = msn.getBuddyGroup().getAllowList();
        for (Iterator iter = list.iterator(); iter.hasNext();) {
            MsnFriend friend = (MsnFriend) iter.next();
            contacts.add(new Contact(new String(friend.getFriendlyName().getBytes(), "UTF-8"), friend.getLoginName()));
        }
        logout();
        return contacts;
    } catch (Exception e) {
        throw new ContactsException("msn protocol has changed", e);
    }
}
  1. 登出msn
private void logout() {
    fixedLogout(msn);
}

public void fixedLogout(MSNMessenger messenger) {
    if (messenger != null) {
        Thread leakedThread = null;
        try {
            leakedThread = getLeakedThread(messenger);
            messenger.logout();
        } catch (Exception ignore) {
        } finally {
            if (leakedThread != null) {
                if (!leakedThread.isInterrupted()) {
                    leakedThread.interrupt();
                }
            }
        }
    }
}

private Thread getLeakedThread(MSNMessenger messenger) {
    try {
        Field nsField = MSNMessenger.class.getDeclaredField("ns");
        nsField.setAccessible(true);
        NotificationProcessor ns = (NotificationProcessor) nsField.get(messenger);
        if (ns == null)
            return null;
        Field callbackField = NotificationProcessor.class.getDeclaredField("callbackCleaner");
        callbackField.setAccessible(true);
        return (Thread) callbackField.get(ns);
    } catch (SecurityException e) {
        throw new RuntimeException("unexpected", e);
    } catch (NoSuchFieldException e) {
        throw new RuntimeException("unexpected", e);
    } catch (IllegalAccessException e) {
        throw new RuntimeException("unexpected", e);
    }
}

msnmlib默认的logout方法总是无法正常登出,导致线程被挂死。google了一下才找到上面的解决方法。

由于msnmlib良好的api接口,使得导入msn联系人列表也变得非常简单了。

Posted in  contact-list msn java


default_scope影响attribute的default值

之前有个需求,发表的文章需要审核之后才能显示,于是在Post类中加了一个default_scope

default_scope :order => 'updated_at desc', :conditions => {:verify => true}

之后就发觉每次创建的post对象,其verify值总是为true,除非手动设置verify=false。当然我在migration的时候已经设置verify的default为false了。很奇怪,于是看了下rails的源代码,其中是这么定义default_scope的

def default_scope(options = {})
  self.default_scoping << { :find => options, :create => options[:conditions].is_a?(Hash) ? options[:conditions] : {} }
end

这里可以看到如果default_scope的conditions是一个Hash的话,那么这个Hash会被保存起来,并在对象initialize的时候生效

def initialize(attributes = nil)
  @attributes = attributes_from_column_definition
  @attributes_cache = {}
  @new_record = true
  ensure_proper_type
  self.attributes = attributes unless attributes.nil?
  self.class.send(:scope, :create).each { |att,value| self.send("#{att}=", value) } if self.class.send(:scoped?, :create)
  result = yield self if block_given?
  callback(:after_initialize) if respond_to_without_attributes?(:after_initialize)
  result
end

注意第7行,可以看到,在实例化Post对象时,会根据conditions的Hash值设置其verify=true。

原来如此,解决方案自然是把conditions的值从Hash改成Array即可

default_scope :order => 'updated_at desc', :conditions => ['verify = ?', true]

Posted in  rails activerecord


Fork me on GitHub