Oh!Coder

Coding Life

AASM Gem简介

| Comments

今天给大家介绍的gem名字叫AASM,是一个状态管理gem,可以更好的帮助你管理业务在不同时间的不同状态。AASM最开始是作为acts_as_state_machine的一个插件,但是后来变得越来越通用,目标不再只作为基于ActiveRecord的model。当前AASM已经可以适配AcitveRecordMongoid以及Mongomapper,但有一点需要注意的是,AASM可以为任何Ruby类所调用,跟调用它的父类无关。

安装

使用gem安装

1
% gem install aassm

使用Bundler安装

1
2
# Gemfile
gem 'aasm'

使用

添加一个状态机非常简单,只要包含AASM的module,并且同时定义statesevents以及它们的transitions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Job
  include AASM

  aasm do
    state :sleeping, :initial => true
    state :running
    state :cleaning

    event :run do
      transitions :from => :sleeping, :to => :running
    end

    event :clean do
      transitions :from => :running, :to => :cleaning
    end

    event :sleep do
      transitions :from => [:running, :cleaning], :to => :sleeping
    end
  end

end

上面这段代码为Job类型提供了成对的实例方法:

1
2
3
4
5
6
7
8
job = Job.new
job.sleeping? # => true
job.may_run?  # => true
job.run
job.running?  # => true
job.sleeping? # => false
job.may_run?  # => false
job.run       # => raises AASM::InvalidTransition

如果你更喜欢简单的truefalse而不喜欢添加一些额外的信息,不用为此发牢骚,可以直接告诉AASM:

1
2
3
4
5
6
7
8
9
10
class Job
  ...
  aasm :whiny_transitions => false do
    ...
  end
end

job.running?  # => true
job.may_run?  # => false
job.run       # => false

当你想要触发一个事件的时候,可以为相应的方法传递一个block,只有当过度状态成功触发,才会调用相应的方法:

1
2
3
job.run do
  job.user.notify_job_ran # Will be called if job.may_run? is true
end

callback

可以为过度状态定义一系列的callback。当遇到一个确定的过度状态时候,这些方法就会被调用,比如进入一个特定的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Job
  include AASM

  aasm do
    state :sleeping, :initial => true, :before_enter => :do_something
    state :running

    event :run, :after => :notify_somebody do
      transitions :from => :sleeping, :to => :running, :after => Proc.new {|*args| set_process(*args) } do
        before do
          log('Preparing to run')
        end
      end
    end

    event :sleep do
      after do
        ...
      end
      error do |e|
        ...
      end
      transitions :from => :running, :to => :sleeping
    end
  end

  def set_process(name)
    ...
  end

  def do_something
    ...
  end

  def notify_somebody(user)
    ...
  end

end

在这个例子中,进入sleeping状态之前do_something方法会被调用,在run动作完成之后(从sleepingrunning),notify_somebody方法会被调用。

这里你可以看到一个回调轻单,以及它们的回调顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
begin
  event           before
  event           guards
  transition      guards
  old_state       before_exit
  old_state       exit
  transition      after
  new_state       before_enter
  new_state       enter
  ...update state...
  event         success             # if persist successful
  old_state       after_exit
  new_state       after_enter
  event           after
rescue
  event           error
end

而且,你可以向事件中传递参数:

1
2
job = Job.new
job.run(:running, :defragmentation)

在这个例子中,当传递:defragmentation时,set_process会被调用。

注意当为一个状态传递参数时,第一个参数必须是所期望的最终状态。上面的例子中,我们希望向:running状态传递名为:defragmentation的参数。当然,你还可以在第一个参数中传入nil,AASM会试图传递参数到第一个获取此参数的事件中。

如果在error事件中处理错误的过程中,对error进行了妥当处理并且把参数传入了:error的回调中,error回调本身可以进行处理或引起进一步的error。

在特定状态的:after回调中,你可以访问源状态(from-state)和目标状态(to state),比如:

1
2
3
def set_process(name)
  logger.info "from #{aasm.from_state} to #{aasm.to_state}"
end

触发当前状态

在事件回调的过程中,你可以很容易的通过aassm.current_event来触发当前事件:

1
2
3
4
# taken the example callback from above
def do_something
  puts "triggered #{aasm.current_event}"
end

然后

1
2
3
4
5
6
7
job = Job.new

# without bang
job.sleep # => triggered :sleep

# with bang
job.sleep! # => triggered :sleep!

警卫

让我们假设你希望只在给定条件下允许触发特定的状态。为了达到这个目的,可以为每个状态设定一个警卫,警卫会在触发事件之前运行。如果警卫的返回值为false,那么事件会被禁止运行(调起AASM::InvalidTransition 或自身返回false):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Cleaner
  include AASM

  aasm do
    state :idle, :initial => true
    state :cleaning

    event :clean do
      transitions :from => :idle, :to => :cleaning, :guard => :cleaning_needed?
    end

    event :clean_if_needed do
      transitions :from => :idle, :to => :cleaning do
        guard do
          cleaning_needed?
        end
      end
      transitions :from => :idle, :to => :idle
    end
  end

  def cleaning_needed?
    false
  end
end

job = Cleaner.new
job.may_clean?            # => false
job.clean                 # => raises AASM::InvalidTransition
job.may_clean_if_needed?  # => true
job.clean_if_needed!      # idle

还可以设置多个警卫,当全部成功执行之后事件才会触发。

1
2
3
4
5
def walked_the_dog?; ...; end

event :sleep do
  transitions :from => :running, :to => :sleeping, :guards => [:cleaning_needed?, :walked_the_dog?]
end

如果你想在同一个警卫中对所有事件进行设置,那么你可以使用事件警卫

1
2
3
4
event :sleep, :guards => [:walked_the_dog?] do
  transitions :from => :running, :to => :sleeping, :guards => [:cleaning_needed?]
  transitions :from => :cleaning, :to => :sleeping
end

如果你更喜欢Ruby的书写方式,也可以使用ifunless

1
2
3
4
5
6
7
8
  event :clean do
    transitions :from => :running, :to => :cleaning, :if => :cleaning_needed?
  end

  event :sleep do
    transitions :from => :running, :to => :sleeping, :unless => :cleaning_needed?
  end
end

状态过度

对于同一个事件里有多个过度状态的情况,当第一个过度状态成功完成之后将会停止执行同一事件内的其他状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
require 'aasm'

class Job
  include AASM

  aasm do
    state :stage1, :initial => true
    state :stage2
    state :stage3
    state :completed

    event :stage1_completed do
      transitions from: :stage1, to: :stage3, guard: :stage2_completed?
      transitions from: :stage1, to: :stage2
    end
  end

  def stage2_completed?
    true
  end
end

job = Job.new
job.stage1_completed
job.aasm.current_state # stage3

ActiveRecord

AASM生来就支持ActiveRecord,并且允许自动把对象的状态保存到数据库中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Job < ActiveRecord::Base
  include AASM

  aasm do # default column: aasm_state
    state :sleeping, :initial => true
    state :running

    event :run do
      transitions :from => :sleeping, :to => :running
    end

    event :sleep do
      transitions :from => :running, :to => :sleeping
    end
  end

end

可以告诉AASM自动保存对象或者不进行保存。

1
2
3
job = Job.new
job.run   # not saved
job.run!  # saved

Job类中,存储包括运行过程中的所有验证。如果你想确保状态在没有运行验证的情况下进行保存,只需要简单的告诉AASM跳过验证即可。注意,当跳过验证之后,数据库中只有列状态会被更新(好比是ActiveRecord的change_column起作用一样)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Job < ActiveRecord::Base
  include AASM

  aasm :skip_validation_on_save => true do
    state :sleeping, :initial => true
    state :running

    event :run do
      transitions :from => :sleeping, :to => :running
    end

    event :sleep do
      transitions :from => :running, :to => :sleeping
    end
  end

end

如果你想确保AASM列根据状态排序而不是直接分配,可以对AASM进行配置,不允许进行直接分配,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Job < ActiveRecord::Base
  include AASM

  aasm :no_direct_assignment => true do
    state :sleeping, :initial => true
    state :running

    event :run do
      transitions :from => :sleeping, :to => :running
    end
  end

end

结果是:

1
2
3
4
job = Job.create
job.aasm_state # => 'sleeping'
job.aasm_state = :running # => raises AASM::NoDirectAssignmentError
job.aasm_state # => 'sleeping'

ActiveRecord枚举

对于Rails 4.1以上的版本,可以使用枚举来标记状态列:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Job < ActiveRecord::Base
  include AASM

  enum state: {
    sleeping: 5,
    running: 99
  }

  aasm :column => :state, :enum => true do
    state :sleeping, :initial => true
    state :running
  end
end

你可以准确的传递一个方法名,方法的名称可以作为一个enum数值,直接映射对枚举进行访问,或者干脆直接把它简单的设置成true。在随后的例子中,AASM将会尝试使用复数列名来尽可能的访问枚举状态。

而且,如果你的列是一个整型类型,可以删除:enum设定—AASM自动检测并且打开枚举支持。如果出现任何错误,可以关闭枚举功能并且通过把:enum设定为false来回退到默认行为。

Sequel

AASM除了ActiveRecord,Mogonid以及MongoMapper以外,还支持Sequel

1
2
3
4
5
6
7
class Job < Sequel::Model
  include AASM

  aasm do # default column: aasm_state
    ...
  end
end

然而,当前只能作为ActiveRecord的不完整特性。比如,这里还有一些级联定义。详见Automatic Scopes

Mongoid

如果你正在使用Mongoid,AASM还支持Mongodb的数据持久化。在你包含AASM之前,确保先包含Mongoid::Document。

1
2
3
4
5
6
7
8
class Job
  include Mongoid::Document
  include AASM
  field :aasm_state
  aasm do
    ...
  end
end

MongoMapper

如果你正在使用MongoMapper,AASM还支持Mongodb的持久化。确保在包含AASM之前先包含MongoMapper::Document。

1
2
3
4
5
6
7
8
9
class Job
  include MongoMapper::Document
  include AASM

  key :aasm_state,                   Symbol
  aasm do
    ...
  end
end

自动级联

AASM会自动为每一个model的状态创建级联方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Job < ActiveRecord::Base
  include AASM

  aasm do
    state :sleeping, :initial => true
    state :running
    state :cleaning
  end

  def self.sleeping
    "This method name is already in use"
  end
end
1
2
3
4
5
6
7
8
class JobsController < ApplicationController
  def index
    @running_jobs = Job.running
    @recent_cleaning_jobs = Job.cleaning.where('created_at >=  ?', 3.days.ago)

    # @sleeping_jobs = Job.sleeping   #=> "This method name is already in use"
  end
end

如果你需要级联,在定义AASM状态的时候做一些设置,关闭创建行为即可,比如:

1
2
3
4
5
6
7
8
9
class Job < ActiveRecord::Base
  include AASM

  aasm :create_scopes => false do
    state :sleeping, :initial => true
    state :running
    state :cleaning
  end
end

支持事务

自AASM的3.0.13版本,开始支持ActiveRecord的事务。所以当一个事务回调或状态更新失败,对数据库的所有改变都将做出回滚。Mongodb并不支持回滚。

如果你想确保只有在事务成功执行之后触发某个action,可以使用after_commit回调,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Job < ActiveRecord::Base
  include AASM

  aasm do
    state :sleeping, :initial => true
    state :running

    event :run, :after_commit => :notify_about_running_job do
      transitions :from => :sleeping, :to => :running
    end
  end

  def notify_about_running_job
    ...
  end
end

如果你想把状态的变化封装到一个事务中,事务之间的嵌套可能会让你感到困惑。关于这个,如果你想了解更多,可以参见ActiveRecord嵌套事务。尽管如此,AASM默认还是需要一个新的事务transaction(:requires_new => true)。可以通过改变配置来覆盖此行为:

1
2
3
4
5
6
7
8
9
class Job < ActiveRecord::Base
  include AASM

  aasm :requires_new_transaction => false do
    ...
  end

  ...
end

数据表列名 & migration

默认情况下,AASM会使用aaswm_state作为列名存储状态。你可以通过使用:column重新定义此列名:

1
2
3
4
5
6
7
8
class Job < ActiveRecord::Base
  include AASM

  aasm :column => 'my_state' do
    ...
  end

end

无论使用什么列名,确保为此次列名的修改创建一个migration(列的类型为string):

1
2
3
4
5
6
7
8
9
class AddJobState < ActiveRecord::Migration
  def self.up
    add_column :jobs, :aasm_state, :string
  end

  def self.down
    remove_column :jobs, :aasm_state
  end
end

检查

AASM支持一些方法用来找出提供或禁止了哪些状态或事件。

给定一个Job类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# show all states
Job.aasm.states.map(&:name)
=> [:sleeping, :running, :cleaning]

job = Job.new

# show all permitted (reachable / possible) states
job.aasm.states(:permitted => true).map(&:name)
=> [:running]
job.run
job.aasm.states(:permitted => true).map(&:name)
=> [:cleaning, :sleeping]

# show all possible (triggerable) events (allowed by transitions)
job.aasm.events.map(&:name)
=> [:sleep]

更多,更新

更新更详细的相关内容,可参见原文档

Comments