Pesquisas apoiadas por modelo em Rails. Um exemplo fantástico:
## app/searches/post_search.rb
class PostSearch < TalentScout :: ModelSearch
criteria :title_includes do | string |
where ( "title LIKE ?" , "% #{ string } %" )
end
criteria :within , choices : {
"Last 24 hours" => 24 . hours ,
"Past Week" => 1 . week ,
"Past Month" => 1 . month ,
"Past Year" => 1 . year ,
} do | duration |
where ( "created_at >= ?" , duration . ago )
end
criteria :only_published , :boolean , default : true do | only |
where ( "published" ) if only
end
order :created_at , default : :desc
order :title
end
## app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
@search = model_search
@posts = @search . results
end
end
<!-- app/views/posts/index.html.erb -->
<%= form_with model : @search , local : true , method : :get do | form | %>
<%= form . label :title_includes %>
<%= form . text_field :title_includes %>
<%= form . label :within %>
<%= form . select :within , @search . each_choice ( :within ) , include_blank : true %>
<%= form . label :only_published %>
<%= form . check_box :only_published %>
<%= form . submit %>
<% end %>
< table >
< thead >
< tr >
< th >
<%= link_to_search "Title" , @search . toggle_order ( :title ) %>
<%= img_tag " #{ @search . order_directions [ :title ] || "unsorted" } _icon.png" %>
</ th >
< th >
<%= link_to_search "Time" , @search . toggle_order ( :created_at ) %>
<%= img_tag " #{ @search . order_directions [ :created_at ] || "unsorted" } _icon.png" %>
</ th >
</ tr >
</ thead >
< tbody >
<% @posts . each do | post | %>
< tr >
< td > <%= link_to post . title , post %> </ td >
< td > <%= post . created_at %> </ td >
</ tr >
<% end %>
</ tbody >
</ table >
No exemplo acima:
PostSearch
cuida da responsabilidade de pesquisar modelos Post
. Ele pode aplicar qualquer combinação de critérios definidos, ignorando automaticamente valores de entrada ausentes, em branco ou inválidos. Também pode ordenar os resultados por uma de suas ordens definidas, na direção ascendente ou descendente.PostsController#index
usa o auxiliar model_search
para construir uma instância PostSearch
e a atribui à variável @search
para uso posterior na visualização. Os resultados da pesquisa também são atribuídos a uma variável para uso na visualização.@search
. O auxiliar link_to_search
é usado para criar links no cabeçalho da tabela que classificam os resultados. Observe que o método toggle_order
usado aqui retorna um novo objeto de pesquisa, deixando @search
inalterado.Para obter uma explicação detalhada dos métodos usados neste exemplo, consulte a documentação da API.
Você pode usar o gerador talent_scout:search
para gerar uma definição de classe de pesquisa de modelo. Por exemplo,
$ rails generate talent_scout:search post
Irá gerar um arquivo "app/searches/post_search.rb" contendo:
class PostSearch < TalentScout :: ModelSearch
end
As classes de pesquisa herdam de TalentScout::ModelSearch
. A classe do modelo de destino é inferida a partir do nome da classe de pesquisa. Por exemplo, PostSearch
procurará modelos Post
por padrão. Para substituir esta inferência, use ModelSearch::model_class=
:
class EmployeeSearch < TalentScout :: ModelSearch
self . model_class = Person # search for Person models instead of `Employee`
end
Os critérios de pesquisa são definidos com o método ModelSearch::criteria
. As definições de critérios podem ser especificadas de três maneiras: com uma cláusula where implícita, com um bloco de consulta explícito ou com uma referência de escopo de modelo. Para ilustrar, os três critérios :title
a seguir são todos equivalentes:
class Post < ActiveRecord :: Base
scope :title_equals , -> ( string ) { where ( title : string ) }
end
class PostSearch < TalentScout :: ModelSearch
criteria :title
criteria :title do | string |
where ( title : string )
end
criteria :title , & :title_equals
end
Observe que os blocos de consulta explícitos são avaliados no contexto do ActiveRecord::Relation
do modelo, assim como os blocos scope
do Active Record.
Uma definição de critério pode especificar um tipo de dados, o que faz com que seu valor de entrada seja digitado antes de ser passado para o bloco ou escopo de consulta. Por exemplo:
class PostSearch < TalentScout :: ModelSearch
criteria :created_on , :date do | date |
where ( created_at : date . beginning_of_day .. date . end_of_day )
end
end
PostSearch . new ( created_on : "Dec 31, 1999" )
Aqui, a string "Dec 31, 1999"
passada para o construtor PostSearch
é convertida em uma Date
antes de ser passada para o bloco de consulta.
O tipo de critério padrão é :string
, o que significa que, por padrão, todos os valores de entrada serão convertidos em strings. Este padrão (em oposição a um padrão sem conversão de tipo) garante um comportamento consistente, não importa como o objeto de pesquisa é construído, seja a partir de valores fortemente digitados ou de parâmetros de solicitação de formulário de pesquisa.
Os tipos de critérios disponíveis são os mesmos dos atributos do Modelo Ativo: :big_integer
, :boolean
, :date
, :datetime
, :decimal
, :float
, :integer
, :string
, :time
, além de quaisquer tipos personalizados que você definir.
Um tipo de conveniência adicional também está disponível: :void
. O tipo :void
é convertido como :boolean
, mas evita que os critérios sejam aplicados quando o valor digitado é falsey. Por exemplo:
class PostSearch < TalentScout :: ModelSearch
criteria :only_edited , :void do
where ( "modified_at > created_at" )
end
end
# The following will apply `only_edited`:
PostSearch . new ( only_edited : true )
PostSearch . new ( only_edited : "1" )
# The following will skip `only_edited`:
PostSearch . new ( only_edited : false )
PostSearch . new ( only_edited : "0" )
PostSearch . new ( only_edited : "" )
Em vez de especificar um tipo, uma definição de critério pode especificar escolhas. As escolhas definem um conjunto de valores que podem ser passados para o bloco de consulta.
As escolhas podem ser especificadas como uma matriz de valores amigáveis:
class PostSearch < TalentScout :: ModelSearch
criteria :category , choices : %w[ Science Tech Engineering Math ] do | name |
where ( category : name . downcase )
end
end
...Ou como um Hash com chaves amigáveis:
class PostSearch < TalentScout :: ModelSearch
criteria :within , choices : {
"Last 24 hours" => 24 . hours ,
"Past Week" => 1 . week ,
"Past Month" => 1 . month ,
"Past Year" => 1 . year ,
} do | duration |
where ( "created_at >= ?" , duration . ago )
end
end
O valor passado para o bloco de consulta será um dos valores do Array ou um dos valores do Hash. O objeto de pesquisa pode ser construído com qualquer um dos valores Array ou chaves Hash ou valores Hash:
PostSearch . new ( category : "Math" )
PostSearch . new ( within : "Last 24 hours" )
PostSearch . new ( within : 24 . hours )
Mas se for especificada uma escolha inválida, os critérios correspondentes não serão aplicados:
# The following will skip the criteria, but will not raise an error:
PostSearch . new ( category : "Marketing" )
PostSearch . new ( within : 12 . hours )
Uma definição de critério pode especificar um valor padrão, que será passado para o bloco de consulta quando o valor de entrada estiver faltando. Os valores padrão também aparecerão nos formulários de pesquisa.
class PostSearch < TalentScout :: ModelSearch
criteria :within_days , :integer , default : 7 do | num |
where ( "created_at >= ?" , num . days . ago )
end
end
# The following are equivalent:
PostSearch . new ( )
PostSearch . new ( within_days : 7 )
Um critério não será aplicado se alguma das seguintes afirmações for verdadeira:
O valor de entrada dos critérios está ausente e nenhum valor padrão foi especificado.
O objeto de pesquisa foi construído com ActionController::Parameters
(em vez de Hash) e o valor de entrada do critério está blank?
e nenhum valor padrão foi especificado. (Esse comportamento evita que campos vazios do formulário de pesquisa afetem os resultados da pesquisa.)
A conversão do tipo do valor de entrada dos critérios falha. Por exemplo:
class PostSearch < TalentScout :: ModelSearch
criteria :created_on , :date do | date |
where ( created_at : date . beginning_of_day .. date . end_of_day )
end
end
# The following will skip `created_on`, but will not raise an error:
PostSearch . new ( created_on : "BAD" )
A definição do critério especifica type :void
e o valor de entrada digitado é falsey.
A definição dos critérios especifica opções e o valor de entrada não é uma escolha válida.
O bloco de consulta de critérios retorna nil
. Por exemplo:
class PostSearch < TalentScout :: ModelSearch
criteria :minimum_upvotes , :integer do | minimum |
where ( "upvotes >= ?" , minimum ) unless minimum <= 0
end
end
# The following will skip the `minimum_upvotes` where clause:
PostSearch . new ( minimum_upvotes : 0 )
Os pedidos de resultados de pesquisa são definidos com o método ModelSearch::order
:
class PostSearch < TalentScout :: ModelSearch
order :created_at
order :title
order :category
end
PostSearch . new ( order : :created_at )
PostSearch . new ( order : :title )
PostSearch . new ( order : :category )
Somente um pedido pode ser aplicado por vez, mas um pedido pode compreender diversas colunas:
class PostSearch < TalentScout :: ModelSearch
order :category , [ :category , :title ]
end
# The following will order by "category, title":
PostSearch . new ( order : :category )
Esse design restrito foi escolhido porque permite classificações de múltiplas colunas com curadoria com interfaces de usuário de classificação de coluna única mais simples e porque evita classificações ad-hoc de várias colunas que podem não ser apoiadas por um índice de banco de dados.
Uma ordem pode ser aplicada na direção ascendente ou descendente. O método ModelSearch#toggle_order
aplicará uma ordem na direção ascendente ou alterará a direção da ordem aplicada de ascendente para decrescente:
class PostSearch < TalentScout :: ModelSearch
order :created_at
order :title
end
# The following will order by "title":
PostSearch . new ( ) . toggle_order ( :title )
PostSearch . new ( order : :created_at ) . toggle_order ( :title )
# The following will order by "title DESC":
PostSearch . new ( order : :title ) . toggle_order ( :title )
Observe que o método toggle_order
não modifica o objeto de pesquisa existente. Em vez disso, ele cria um novo objeto de pesquisa com a nova ordem e os valores dos critérios do objeto de pesquisa anterior.
Quando uma ordem de múltiplas colunas é aplicada na direção decrescente, todas as colunas são afetadas:
class PostSearch < TalentScout :: ModelSearch
order :category , [ :category , :title ]
end
# The following will order by "category DESC, title DESC":
PostSearch . new ( order : :category ) . toggle_order ( :category )
Para contornar esse comportamento e, em vez disso, corrigir uma coluna em uma direção estática, acrescente " ASC"
ou " DESC"
ao nome da coluna:
class PostSearch < TalentScout :: ModelSearch
order :category , [ :category , "created_at ASC" ]
end
# The following will order by "category, created_at ASC":
PostSearch . new ( order : :category )
# The following will order by "category DESC, created_at ASC":
PostSearch . new ( order : :category ) . toggle_order ( :category )
Uma ordem pode ser aplicada diretamente na direção ascendente ou descendente, sem usar toggle_order
, anexando um sufixo apropriado:
class PostSearch < TalentScout :: ModelSearch
order :title
end
# The following will order by "title":
PostSearch . new ( order : :title )
PostSearch . new ( order : "title.asc" )
# The following will order by "title DESC":
PostSearch . new ( order : "title.desc" )
Os sufixos padrão, conforme visto no exemplo acima, são ".asc"
e ".desc"
. Estes foram escolhidos por serem amigáveis ao I18n. Eles podem ser substituídos como parte da definição do pedido:
class PostSearch < TalentScout :: ModelSearch
order "Title" , [ :title ] , asc_suffix : " (A-Z)" , desc_suffix : " (Z-A)"
end
# The following will order by "title":
PostSearch . new ( order : "Title" )
PostSearch . new ( order : "Title (A-Z)" )
# The following will order by "title DESC":
PostSearch . new ( order : "Title (Z-A)" )
Um pedido pode ser designado como o pedido padrão, o que fará com que esse pedido seja aplicado quando nenhum pedido for especificado de outra forma:
class PostSearch < TalentScout :: ModelSearch
order :created_at , default : :desc
order :title
end
# The following will order by "created_at DESC":
PostSearch . new ( )
# The following will order by "created_at":
PostSearch . new ( order : :created_at )
# The following will order by "title":
PostSearch . new ( order : :title )
Observe que a direção padrão da ordem pode ser crescente ou decrescente especificando default: :asc
ou default: :desc
, respectivamente. Além disso, assim como apenas um pedido pode ser aplicado por vez, apenas um pedido pode ser designado como padrão.
Um escopo de pesquisa padrão pode ser definido com ModelSearch::default_scope
:
class PostSearch < TalentScout :: ModelSearch
default_scope { where ( published : true ) }
end
O escopo padrão será aplicado independentemente dos critérios ou valores de entrada do pedido.
Os controladores podem usar o método auxiliar model_search
para construir um objeto de pesquisa com os parâmetros de consulta da solicitação atual:
class PostsController < ApplicationController
def index
@search = model_search
@posts = @search . results
end
end
No exemplo acima, model_search
constrói um objeto PostSearch
. A classe de pesquisa do modelo é derivada automaticamente do nome da classe do controlador. Para substituir a classe de pesquisa do modelo, use ::model_search_class=
:
class EmployeesController < ApplicationController
self . model_search_class = PersonSearch
def index
@search = model_search # will construct PersonSearch instead of `EmployeeSearch`
@employees = @search . results
end
end
Nestes exemplos, o objeto de pesquisa é armazenado em @search
para uso na visualização. Observe que @search.results
retorna um ActiveRecord::Relation
, portanto, qualquer escopo adicional, como paginação, pode ser aplicado.
Os formulários de pesquisa podem ser renderizados usando o construtor de formulários do Rails e um objeto de pesquisa:
<%= form_with model: @search, local: true, method: :get do |form| %>
<%= form.label :title_includes %>
<%= form.text_field :title_includes %>
<%= form.label :created_on %>
<%= form.date_field :created_on %>
<%= form.label :only_published %>
<%= form.check_box :only_published %>
<%= form.submit %>
<% end %>
Observe o method: :get
argument to form_with
. Isto é necessário.
Os campos do formulário serão preenchidos com os valores de entrada dos critérios (ou padrão) de mesmo nome de @search
. Campos de formulário apropriados ao tipo podem ser usados, por exemplo, date_field
para type :date
, check_box
para tipos :boolean
e :void
, etc.
Por padrão, o formulário será submetido à ação de índice do controlador que corresponde ao model_class
do objeto de pesquisa. Por exemplo, PostSearch.model_class
é Post
, portanto, um formulário com uma instância de PostSearch
será enviado para PostsController#index
. Para alterar para onde o formulário é enviado, use a opção :url
de form_with
.
Os links de pesquisa podem ser renderizados usando o método auxiliar de visualização link_to_search
:
<%= link_to_search "Sort by title", @search.toggle_order(:title, :asc) %>
O link apontará automaticamente para o controlador atual e a ação atual, com parâmetros de consulta do objeto de pesquisa fornecido. Para vincular a um controlador ou ação diferente, passe um Hash de opções no lugar do objeto de pesquisa:
<%= link_to_search "Sort by title", { controller: "posts", action: "index",
search: @search.toggle_order(:title, :asc) } %>
O auxiliar link_to_search
também aceita as mesmas opções HTML que o auxiliar link_to
do Rails:
<%= link_to_search "Sort by title", @search.toggle_order(:title, :asc),
id: "title-sort-link", class: "sort-link" %>
...Bem como um bloco de conteúdo:
<%= link_to_search @search.toggle_order(:title, :asc) do %>
Sort by title <%= img_tag "sort_icon.png" %>
<% end %>
ModelSearch
A classe ModelSearch
fornece vários métodos que são úteis ao renderizar a visualização.
Um desses métodos é ModelSearch#toggle_order
, mostrado em exemplos anteriores. Lembre-se de que toggle_order
é um método estilo construtor que não modifica o objeto de pesquisa. Em vez disso, ele duplica o objeto de pesquisa e define a ordem do novo objeto. Esse comportamento é adequado para gerar links para múltiplas variantes de uma pesquisa, como classificar links em cabeçalhos de colunas de tabelas.
ModelSearch#with
e ModelSearch#without
Dois métodos adicionais de estilo construtor são ModelSearch#with
e ModelSearch#without
. Assim como toggle_order
, ambos os métodos retornam um novo objeto de pesquisa, deixando o objeto de pesquisa original inalterado. O método with
aceita um Hash de valores de entrada de critérios para mesclar sobre o conjunto original de valores de entrada de critérios:
class PostSearch < TalentScout :: ModelSearch
criteria :title
criteria :published , :boolean
end
# The following are equivalent:
PostSearch . new ( title : "Maaaaath!" , published : true )
PostSearch . new ( title : "Maaaaath!" ) . with ( published : true )
PostSearch . new ( title : "Math?" ) . with ( title : "Maaaaath!" , published : true )
O método without
aceita uma lista de valores de entrada de critérios a serem excluídos (os valores de critérios padrão ainda se aplicam):
class PostSearch < TalentScout :: ModelSearch
criteria :title
criteria :published , :boolean , default : true
end
# The following are equivalent:
PostSearch . new ( title : "Maaaaath!" )
PostSearch . new ( title : "Maaaaath!" , published : false ) . without ( :published )
ModelSearch#each_choice
Outro método útil é ModelSearch#each_choice
, que irá iterar sobre as escolhas definidas para um determinado critério. Isso pode ser usado para gerar links para variantes de uma pesquisa:
class PostSearch < TalentScout :: ModelSearch
criteria :category , choices : %w[ Science Tech Engineering Math ]
end
<% @search.each_choice(:category) do |choice, chosen| %>
<%= link_to_search "Category: #{choice}", @search.with(category: choice),
class: ("active" if chosen) %>
<% end %>
Observe que se o bloco passado para each_choice
aceitar dois argumentos, o segundo argumento indicará se a escolha está escolhida no momento.
Se nenhum bloco for passado para each_choice
, ele retornará um Enumerator
. Isso pode ser usado para gerar opções para uma caixa de seleção:
<%= form_with model: @search, local: true, method: :get do |form| %>
<%= form.select :category, @search.each_choice(:category) %>
<%= form.submit %>
<% end %>
O método each_choice
também pode ser invocado com :order
. Fazer isso irá iterar em cada direção de cada ordem definida, produzindo os rótulos apropriados, incluindo o sufixo de direção:
class PostSearch < TalentScout :: ModelSearch
order "Title" , [ :title ] , asc_suffix : " (A-Z)" , desc_suffix : " (Z-A)"
order "Time" , [ :created_at ] , asc_suffix : " (oldest first)" , desc_suffix : " (newest first)"
end
<%= form_with model: @search, local: true, method: :get do |form| %>
<%= form.select :order, @search.each_choice(:order) %>
<%= form.submit %>
<% end %>
A caixa de seleção no formulário acima listará quatro opções: "Título (AZ)", "Título (ZA)", "Hora (mais antiga primeiro)", "Hora (mais recente primeiro)".
ModelSearch#order_directions
Finalmente, o método auxiliar ModelSearch#order_directions
retorna um HashWithIndifferentAccess
refletindo a direção atualmente aplicada de cada pedido definido. Ele contém uma chave para cada ordem definida e associa cada chave a :asc
, :desc
ou nil
.
class PostSearch < TalentScout :: ModelSearch
order "Title" , [ :title ]
order "Time" , [ :created_at ]
end
< thead >
< tr >
<% @search . order_directions . each do | order , direction | %>
< th >
<%= link_to_search order , @search . toggle_order ( order ) %>
<%= img_tag " #{ direction || "unsorted" } _icon.png" %>
</ th >
<% end %>
</ tr >
</ thead >
Lembre-se que apenas uma ordem pode ser aplicada por vez, portanto apenas um valor no Hash, no máximo, será diferente de nil
.
Adicione a gema ao seu Gemfile:
$ bundle add talent_scout
E execute o gerador de instalação:
$ rails generate talent_scout:install
Execute bin/test
para executar os testes.
Licença MIT