postgres数组字段上的ActiveAdmin过滤器

时间:2015-12-13 11:34:13

标签: ruby-on-rails ruby ruby-on-rails-4 rubygems activeadmin

我在ActiveAdmin中添加了以下过滤器。

filter :roles, as: :select, collection Model::ROLES, multiple: true

但是当我选择过滤器值来搜索角色时。它给了我以下错误

PG::InvalidTextRepresentation: ERROR:  malformed array literal: "teacher"LINE 1: ...ted" = $1 AND roles" IN('teacher
DETAIL:  Array value must start with "{" or dimension information.                                                             ^

有什么想法吗?我们如何使用AA过滤器搜索/过滤ARRAY字段?我正在使用 Rails 4.2.4, 红宝石2.2.2p95

4 个答案:

答案 0 :(得分:0)

我想出了一个略有不同(并受其启发)的解决方案:https://stackoverflow.com/a/45728004/1170086

矿井涉及一些变更(在其他情况下,请防止破坏contains运算符)。因此,您将基本上创建两个初始化程序文件:

此代码用于Arel,以支持给定表列的@>运算符(PG中的数组包含运算符)。

# config/initializers/arel.rb

module Arel
  class Nodes::ContainsArray < Arel::Nodes::Binary
    def operator
      :"@>"
    end
  end

  class Visitors::PostgreSQL
    private

    def visit_Arel_Nodes_ContainsArray(o, collector)
      infix_value o, collector, ' @> '
    end
  end

  module Predications
    def contains(other)
      Nodes::ContainsArray.new self, Nodes.build_quoted(other, self)
    end
  end
end

另一个文件旨在创建一个新的Ransack谓词,但我还决定支持:array类型(Ransack本身不支持谓词)。

# config/initializers/ransack.rb

module Ransack
  module Nodes
    class Value < Node
      alias_method :original_cast, :cast

      def cast(type)
        return Array(value) if type == :array
        original_cast(type)
      end
    end
  end
end

Ransack.configure do |config|
  config.add_predicate 'contains_array',
    arel_predicate: 'contains',
    formatter: proc { |v| "{#{v.join(',')}}" },
    validator: proc { |v| v.present? },
    type: :array
end

并在其他使用它。您所需要做的就是:

User.ransack(roles_contains_array: %i[admin manager])

或作为ActiveAdmin中的过滤器(这是我的情况):

ActiveAdmin.register User do
  # ...
  filter :roles_contains_array, as: :select, collection: User.roles_for_select
  # ...
end

我希望它对我有用。 ;)

答案 1 :(得分:0)

leandroico解决方案效果很好。

但是如果您使用此格式化程序添加谓词

formatter: proc { |v| "{#{v.join(', ')}}" },(注意逗号后的空格)

然后,您可以在过滤器输入中使用multiple: true关键字,并按多个值过滤:

filter :roles_contains_array, as: :select, multiple: true, collection: User.roles_for_select

答案 2 :(得分:0)

我使用@leandroico的答案提出了以下wiki类型的方法来实现此目的。

如何为ActiveAdmin创建自定义SQL搜索(使用Arel和Ransack)

在ActiveAdmin中,过滤器在app/admin/model.rb中声明,例如:

ActiveAdmin.register Model do
  filter 'column_name', label: 'column_name', as: :string
end

这将使搜索框在前端可用,并提供可供选择的选项

contains
equals
starts with
ends with

您甚至可以做类似...

filter 'column_name_contains', label: 'column_name', as: :string

...仅在前端提供contains类型的搜索。

您还可以(在其他地方定义一些自定义方法之后)指定其他非内置的搜索方法,例如:

filter 'column_name_custom_contains', label: 'column_name', as: :string


本文档的其余部分将介绍如何定义此自定义搜索方法custom_contains

config/initializers/arel.rb中,定义以下内容:

  module Arel
    # this example of custom_contains will cast the SQL column as ::text and then do a wildcard-wrapped ILIKE

    class Nodes::CustomContains < Arel::Nodes::Binary
      def operator
        '::text ILIKE'.to_sym
      end
    end

    class Visitors::PostgreSQL
      private
      def visit_Arel_Nodes_CustomContains(o, collector)
        infix_value o, collector, '::text ILIKE '
      end
    end

    module Predications
      def custom_contains(column_value)
        column_value = self.relation.engine.column_types[self.name.to_s].type_cast_for_database(column_value)
        column_value = "%#{self.relation.engine.send(:sanitize_sql_like, column_value)}%"  # wrap escaped value with % wildcard
        column_value = Nodes.build_quoted(column_value, self)
        Nodes::CustomContains.new(self, column_value)
      end
    end
  end

  module ActiveRecord::QueryMethods
    def custom_contains(predicates)
      return none if predicates.length == 0
      predicates.map{ |column_name, column_value|
        column_value = table.engine.column_types[column_name.to_s].type_cast_for_database(column_value)
        column_value = "%#{table.engine.send(:sanitize_sql_like, column_value)}%"  # wrap escaped value with % wildcard
        column_value = Arel::Nodes.build_quoted(column_value)
        where Arel::Nodes::CustomContains.new(table[column_name], column_value)
      }.inject(:merge)
    end
  end

  module ActiveRecord::Querying
    delegate :custom_contains, :to => :all
  end

config/initializers/ransack.rb中,定义以下内容:

  Ransack.configure do |config|
    config.add_predicate(
      'custom_contains',
      arel_predicate: 'custom_contains',
      formatter: proc { |v| v.to_s },
      validator: proc { |v| v.present? },
      type: :string
    )
  end

上面完成了几件事:

1)您可以使用delegateall ActiveRecord模型中的custom_contains方法:

puts Model.custom_contains(column_name: 'search for me').to_sql

2)您可以使用Ransack搜索已定义的Arel谓词:

puts Model.ransack(column_name_custom_contains: 'search for me').result.to_sql

但是,为了在ActiveAdmin中执行以下操作...

filter 'column_name_custom_contains', label: 'column_name', as: :string

...我们必须为Model添加一个范围,以便在column_name_custom_contains上有一个方法Model

  scope_name = "#{column_name}_custom_contains".to_sym
  unless Model.methods.include?(scope_name)
    Model.scope(
      scope_name,
      ->(value) {
        Model.custom_contains({column_name.to_sym => value})
      }
    )
  end

Voila!

答案 3 :(得分:0)

您可以设置一个自定义的 ransacker 方法,首先使用常规 postgres 搜索收集您想要返回的 id,然后根据这些 id 返回结果:

class ListOfItems(private val list: Map<String, Item> = mapOf()) : Parcelable, List<Item> by list.values.toList() {
    operator fun get(name: String) = list[name] ?: Item(name)
}

如果您的过滤器是一个选择下拉菜单,那么这应该可以正常工作。如果您有自由格式的文本框,请确保使用“in”谓词:

class User < ApplicationRecord

  ransacker :roles,
    formatter: proc { |str|
      data = where("? = ANY (roles)", str).map(&:id)
      data.present? ? data : nil
    } do |parent|
      parent.table[:id]
    end

end