如何为普通 Ruby 数组创建可组合范围
How to create composable scopes for plain Ruby arrays
我真正喜欢 Active Record 的一件事是它的命名范围,并且能够将范围链接在一起以构建富有表现力的查询。
用普通 Ruby Enumerables/Arrays 实现此目的的类似方法是什么,最好不要以任何危险的方式对 Enumerable 或 Array 进行猴子修补?
例如:
### ActiveRecord Model
class User < ActiveRecord::Base
scope :customers, -> { where(:role => 'customer') }
scope :speaking, ->(lang) { where(:language => lang) }
end
# querying
User.customers.language('English') # all English customers
### Plain-Ruby Array
module User
class << self
def customers
users.select { |u| u[:role] == 'customer' }
end
def speaking(lang)
users.select { |u| u[:language] == lang }
end
private
def users
[
{:name => 'John', :language => 'English', :role => 'customer'},
{:name => 'Jean', :language => 'French', :role => 'customer'},
{:name => 'Hans', :language => 'German', :role => 'user'},
{:name => 'Max', :language => 'English', :role => 'user'}
]
end
end
end
User.customers # all customers
User.language('English') # all English speakers
# how do I achieve something similar to User.customers.language('English') ...?
我知道我可以在模块内构建一个方法 customers_with_language
,但我正在寻找一种通用方法来使用任意数量的 "scopes".
来解决这个问题
这是一个 ScopableArray
的粗略实现,它继承了 Array
:
class ScopableArray < Array
def method_missing(method_sym, *args)
ScopableArray.new(select { |u| u[method_sym] == args[0] } )
end
end
当这个 class 接收到一个它不识别的方法时,它假设你想根据方法名和参数值的字段来过滤它:
users = ScopableArray.new([
{:name => 'John', :language => 'English', :role => 'customer'},
{:name => 'Jean', :language => 'French', :role => 'customer'},
{:name => 'Hans', :language => 'German', :role => 'user'},
{:name => 'Max', :language => 'English', :role => 'user'}
])
users.role('customer')
# => [{:name=>"John", :language=>"English", :role=>"customer"}, {:name=>"Jean", :language=>"French", :role=>"customer"}]
users.role('customer').language('English')
# => [{:name=>"John", :language=>"English", :role=>"customer"}]
您还可以查看 ActiveRecord
's implementation pattern 以获得更详细的方案,您可以在其中通过传递名称和可调用块来定义范围,如下所示:
class ScopableArray2 < Array
class << self
def scope(name, body)
unless body.respond_to?(:call)
raise ArgumentError, 'The scope body needs to be callable.'
end
define_method(name) do |*args|
dup.select! { |x| body.call(x, *args) }
end
end
end
end
那么你可以这样做:
class Users < ScopableArray2
scope :customers, ->(x) { x[:role] == 'customer' }
scope :speaking, ->(x, lang) { x[:language] == lang }
end
users = Users.new([
{:name => 'John', :language => 'English', :role => 'customer'},
{:name => 'Jean', :language => 'French', :role => 'customer'},
{:name => 'Hans', :language => 'German', :role => 'user'},
{:name => 'Max', :language => 'English', :role => 'user'}
])
users.customers.speaking('English')
# => [{:name=>"John", :language=>"English", :role=>"customer"}]
我真正喜欢 Active Record 的一件事是它的命名范围,并且能够将范围链接在一起以构建富有表现力的查询。
用普通 Ruby Enumerables/Arrays 实现此目的的类似方法是什么,最好不要以任何危险的方式对 Enumerable 或 Array 进行猴子修补?
例如:
### ActiveRecord Model
class User < ActiveRecord::Base
scope :customers, -> { where(:role => 'customer') }
scope :speaking, ->(lang) { where(:language => lang) }
end
# querying
User.customers.language('English') # all English customers
### Plain-Ruby Array
module User
class << self
def customers
users.select { |u| u[:role] == 'customer' }
end
def speaking(lang)
users.select { |u| u[:language] == lang }
end
private
def users
[
{:name => 'John', :language => 'English', :role => 'customer'},
{:name => 'Jean', :language => 'French', :role => 'customer'},
{:name => 'Hans', :language => 'German', :role => 'user'},
{:name => 'Max', :language => 'English', :role => 'user'}
]
end
end
end
User.customers # all customers
User.language('English') # all English speakers
# how do I achieve something similar to User.customers.language('English') ...?
我知道我可以在模块内构建一个方法 customers_with_language
,但我正在寻找一种通用方法来使用任意数量的 "scopes".
这是一个 ScopableArray
的粗略实现,它继承了 Array
:
class ScopableArray < Array
def method_missing(method_sym, *args)
ScopableArray.new(select { |u| u[method_sym] == args[0] } )
end
end
当这个 class 接收到一个它不识别的方法时,它假设你想根据方法名和参数值的字段来过滤它:
users = ScopableArray.new([
{:name => 'John', :language => 'English', :role => 'customer'},
{:name => 'Jean', :language => 'French', :role => 'customer'},
{:name => 'Hans', :language => 'German', :role => 'user'},
{:name => 'Max', :language => 'English', :role => 'user'}
])
users.role('customer')
# => [{:name=>"John", :language=>"English", :role=>"customer"}, {:name=>"Jean", :language=>"French", :role=>"customer"}]
users.role('customer').language('English')
# => [{:name=>"John", :language=>"English", :role=>"customer"}]
您还可以查看 ActiveRecord
's implementation pattern 以获得更详细的方案,您可以在其中通过传递名称和可调用块来定义范围,如下所示:
class ScopableArray2 < Array
class << self
def scope(name, body)
unless body.respond_to?(:call)
raise ArgumentError, 'The scope body needs to be callable.'
end
define_method(name) do |*args|
dup.select! { |x| body.call(x, *args) }
end
end
end
end
那么你可以这样做:
class Users < ScopableArray2
scope :customers, ->(x) { x[:role] == 'customer' }
scope :speaking, ->(x, lang) { x[:language] == lang }
end
users = Users.new([
{:name => 'John', :language => 'English', :role => 'customer'},
{:name => 'Jean', :language => 'French', :role => 'customer'},
{:name => 'Hans', :language => 'German', :role => 'user'},
{:name => 'Max', :language => 'English', :role => 'user'}
])
users.customers.speaking('English')
# => [{:name=>"John", :language=>"English", :role=>"customer"}]