Eine Suche mit dem b4um Generator hinzufügen

Eine Suche mit dem b4um Generator hinzufügen

Im vorherigen Tutorial haben wir unsere bestehende Product-Übersicht von einer Bento-Darstellung auf eine responsive Tabelle umgestellt.
Unsere fünf Products werden jetzt übersichtlich mit den Spalten Name, Description, Price und Status dargestellt.
Mit zunehmender Anzahl von Datensätzen stellt sich jedoch eine neue Frage:
Wie findet ein Besucher schnell einen bestimmten Eintrag?
Dafür stellt der b4um Generator eine datenbankgestützte Suche zur Verfügung.
In diesem Tutorial erweitern wir unsere bestehende Product-Ressource mit:
bin/rails generate b4um:search Product
Für dieses Tutorial verwenden wir:
b4um_generators 0.1.0

1. Ausgangspunkt

Unsere bestehende Product-Ressource besitzt weiterhin die Felder:
name:string
description:text
price:decimal
status:string
Außerdem befinden sich bereits fünf Products in unserer Datenbank:
MacBook Pro
iPad Pro
Studio Display
Mac mini
Magic Keyboard
Die Product-Übersicht verwendet aus dem vorherigen Tutorial unsere Tabellenansicht:
<%= render "table", products: @products %>
An dieser bestehenden Ressource setzen wir jetzt an.
Wir müssen weder ein neues Model noch eine neue Tabelle anlegen.

2. Die Suche erzeugen

Im Terminal führen wir aus:
bin/rails generate b4um:search Product
Der Generator meldet:
create   app/controllers/concerns/b4um_search.rb
insert   app/controllers/products_controller.rb
gsub     app/controllers/products_controller.rb
prepend  app/views/products/index.html.erb
Im Gegensatz zum Tabellen-Generator erzeugt b4um:search also nicht nur ein View-Partial.
Der Generator erweitert mehrere Bereiche unserer bestehenden Ressource.

3. Was wurde erzeugt?

Der Generator legt zunächst ein neues Concern an:
app/controllers/concerns/b4um_search.rb
Außerdem wird der vorhandene:
app/controllers/products_controller.rb
angepasst.
Und schließlich ergänzt der Generator automatisch ein Suchformular in:
app/views/products/index.html.erb
Damit ist die Suche direkt mit unserer bestehenden Product-Übersicht verbunden.
Wir müssen das Suchformular nicht manuell erstellen.

4. Das B4umSearch Concern

Die eigentliche Suchlogik befindet sich in:
app/controllers/concerns/b4um_search.rb
Der Generator hat folgende Suchmethode erzeugt:
def b4um_search(scope, query)
  return scope if query.blank?

  searchable_columns = scope.klass.columns.select do |column|
    %i[string text].include?(column.type)
  end

  return scope if searchable_columns.empty?

  table_name = scope.klass.connection.quote_table_name(
    scope.klass.table_name
  )

  conditions = searchable_columns.map do |column|
    column_name = scope.klass.connection.quote_column_name(
      column.name
    )

    "LOWER(#{table_name}.#{column_name}) LIKE :b4um_query"
  end

  pattern = "%#{ActiveRecord::Base.sanitize_sql_like(query.to_s.strip.downcase)}%"

  scope.where(
    conditions.join(" OR "),
    b4um_query: pattern
  )
end
Die Suche ermittelt automatisch die durchsuchbaren Spalten des Models.
Dabei werden Felder vom Typ:
string
text
berücksichtigt.

5. Welche Product-Felder werden durchsucht?

Unser Product besitzt:
name:string
description:text
price:decimal
status:string
Damit werden automatisch folgende Felder durchsucht:
name
description
status
price wird dagegen nicht durchsucht, da dieses Feld vom Typ:
decimal
ist.
Wir müssen die einzelnen durchsuchbaren Spalten also nicht manuell im Controller hinterlegen.

6. Der ProductsController

Der Generator bindet das neue Concern automatisch in unseren Controller ein:
include B4umSearch
Die index-Action verwendet anschließend:
@products = b4um_search(
  Product.all,
  params[:q]
)
Dabei ist:
Product.all
unser Ausgangs-Scope.
Der Suchbegriff wird über:
params[:q]
übergeben.
Ist kein Suchbegriff vorhanden, gibt b4um_search den ursprünglichen Scope zurück.
Unsere normale Product-Liste funktioniert damit weiterhin.

7. Das Suchformular

Auch das Suchformular wurde automatisch in unsere bestehende Index-Seite eingefügt.
Es befindet sich jetzt oberhalb unserer Product-Übersicht:
<div class="b4um-search">
  <%= form_with url: request.path,
                method: :get,
                class: "b4um-search__form" do |form| %>
    <div class="form-field">
      <%= form.search_field :q,
            value: params[:q],
            class: "form-input",
            placeholder: " " %>
      <%= form.label :q, "Search", class: "form-label" %>
    </div>

    <%= form.submit "Search", class: "button button--primary" %>
  <% end %>
</div>
Die Suche verwendet einen normalen GET-Request.
Dadurch erscheint der Suchbegriff auch in der URL.

8. Unsere erste Suche

Jetzt öffnen wir:
http://localhost:3000/products
In der Product-Übersicht befindet sich nun oberhalb der Tabelle ein Suchfeld.
Wir geben ein:
mac
und klicken auf:
Search
Von unseren fünf Products werden jetzt nur noch zwei angezeigt:
MacBook Pro
Mac mini
Die übrigen Products werden aus der aktuellen Ergebnisliste herausgefiltert.

Die mit b4um:search erweiterte Product-Tabelle. Eine Suche nach „mac“ liefert MacBook Pro und Mac mini als Treffer.
Die mit b4um:search erweiterte Product-Tabelle. Eine Suche nach „mac“ liefert MacBook Pro und Mac mini als Treffer.

9. Groß- und Kleinschreibung

Bei unserem Test haben wir bewusst:
mac
kleingeschrieben.
In der Datenbank heißen die Products jedoch:
MacBook Pro
Mac mini
Die Suche findet sie trotzdem.
Der Grund dafür ist der Vergleich über:
LOWER(...)
Auch der eingegebene Suchbegriff wird in Kleinbuchstaben umgewandelt.
Die Suche unterscheidet damit bei diesem Vergleich nicht zwischen beispielsweise:
mac
Mac
MAC

10. Teilbegriffe werden gefunden

Der Suchbegriff muss außerdem nicht mit dem vollständigen Inhalt eines Feldes übereinstimmen.
Das Suchmuster wird mit % vor und hinter dem eingegebenen Begriff aufgebaut.
Dadurch kann:
mac
innerhalb von:
MacBook Pro
gefunden werden.
Das macht die Suche wesentlich praktischer, da Benutzer nicht den vollständigen Namen eines Eintrags kennen müssen.

11. Auch Beschreibungen werden durchsucht

Unsere Suche beschränkt sich nicht auf den Product-Namen.
Da description ein text-Feld ist, wird auch die Beschreibung automatisch berücksichtigt.
Wir testen das mit:
kreative
Das Ergebnis enthält:
MacBook Pro
iPad Pro
Warum?
Die Beschreibung des MacBook Pro enthält:
Leistungsstarkes Notebook für Entwicklung und kreative Projekte.
Beim iPad Pro steht:
Vielseitiges Tablet für kreative Arbeit, Medien und unterwegs.
Der Suchbegriff befindet sich also in beiden Beschreibungen.

b4um:search durchsucht automatisch auch Textfelder. Der Begriff „kreative“ wird in den Beschreibungen von MacBook Pro und iPad Pro gefunden.
b4um:search durchsucht automatisch auch Textfelder. Der Begriff „kreative“ wird in den Beschreibungen von MacBook Pro und iPad Pro gefunden.

12. Der Suchbegriff bleibt erhalten

Nach dem Absenden bleibt der eingegebene Suchbegriff im Suchfeld sichtbar.
Dafür verwendet das Formular:
value: params[:q]
Auch in der URL ist die Suchanfrage erkennbar.
Bei unserer Suche nach mac enthält sie beispielsweise:
q=mac
Damit ist der aktuelle Suchzustand nachvollziehbar und kann bei weiteren Funktionen verwendet werden.
Das wird später insbesondere bei Pagination und Infinite Scroll interessant.

13. Was passiert ohne Suchbegriff?

Das Concern beginnt mit:
return scope if query.blank?
Ist das Suchfeld leer, wird deshalb keine Filterung vorgenommen.
Der ursprüngliche Scope:
Product.all
wird zurückgegeben.
Damit erscheinen wieder alle vorhandenen Products.
Die Suche muss also nicht separat deaktiviert werden.

14. Was passiert ohne Treffer?

Wir testen außerdem einen Begriff, der in keinem unserer Products vorkommt:
Waschmaschine
Die Suche liefert keine Datensätze.
Da anschließend:
@products.any?
nicht mehr erfüllt ist, greift der bereits vorhandene Empty State unserer Product-Index-Seite.
In unserem aktuellen Beispiel erscheint:
No products yet.

Create your first product to get started.

Hinweis

Dieser Empty State stammt aus unserer bestehenden Product-Übersicht.
Bei einer Suche ohne Treffer bedeutet „No products yet“ natürlich nicht, dass überhaupt keine Products vorhanden sind – lediglich die aktuelle Suche hat keine Treffer geliefert.
Wer zwischen „keine Datensätze vorhanden“ und „keine Suchtreffer“ unterscheiden möchte, kann den Empty State später entsprechend anpassen.

15. Sichere LIKE-Suche

Der eingegebene Suchbegriff wird nicht einfach ungeprüft in den SQL-Ausdruck eingesetzt.
Das Concern verwendet:
ActiveRecord::Base.sanitize_sql_like(...)
Dadurch werden Zeichen berücksichtigt, die innerhalb eines SQL-LIKE-Suchmusters eine besondere Bedeutung besitzen.
Anschließend wird der Suchwert als benannter Parameter verwendet:
b4um_query: pattern
Die Suchbedingungen werden also nicht durch direktes Einsetzen der Benutzereingabe zusammengesetzt.

16. Suche und Tabelle arbeiten zusammen

Für dieses Tutorial haben wir unsere Product-Übersicht weiterhin als Tabelle dargestellt:
<%= render "table", products: @products %>
b4um:search verändert diese Darstellung nicht.
Stattdessen verändert die Suche die Collection:
@products
Die Tabelle erhält anschließend einfach die gefilterten Products.
Das ist ein wichtiger Unterschied.
Die Suche ist nicht an das Tabellenlayout gebunden.
Sie filtert die Daten, bevor diese an die View weitergegeben werden.

17. Suche auch mit Bento

Deshalb können wir die Suche ebenso mit unseren Bento-Layouts verwenden.
Wenn wir beispielsweise wieder:
<%= render "bento", products: @products %>
verwenden, erhält das Bento-Partial dieselbe gefilterte Collection.
Die Suchfunktion und die Darstellung bleiben voneinander getrennt.
Damit lässt sich b4um:search sowohl mit:
Bento
Tabelle
und den verschiedenen Bento-Layouts kombinieren.

18. Was der Generator automatisch erledigt

Mit nur:
bin/rails generate b4um:search Product
hat der Generator mehrere Schritte für uns erledigt.
Er hat:
B4umSearch Concern erzeugt
B4umSearch in den ProductsController eingebunden
die index-Action mit der Suche verbunden
das Suchformular in die Product-Übersicht eingefügt
Wir mussten danach keinen zusätzlichen Code schreiben.
Unsere bestehende Tabellenansicht konnte unverändert weiterverwendet werden.

19. Das haben wir gelernt

Mit:
bin/rails generate b4um:search Product
haben wir eine bestehende Product-Ressource um eine datenbankgestützte Suche erweitert.
Die Suche:
durchsucht String- und Textfelder automatisch
findet Teilbegriffe
arbeitet unabhängig von Groß- und Kleinschreibung
lässt leere Suchanfragen unverändert durch
behält den Suchbegriff in der URL
funktioniert mit der bestehenden Tabellenansicht
kann ebenso mit Bento-Darstellungen verwendet werden
Damit können wir unsere Product-Liste jetzt nicht nur unterschiedlich darstellen, sondern auch gezielt filtern.
Im nächsten Tutorial
Unsere Suche funktioniert bereits gut.
Mit aktuell fünf Products ist allerdings noch nicht sichtbar, was passiert, wenn unsere Anwendung später 50, 100 oder mehrere hundert Datensätze enthält.
Als Nächstes kümmern wir uns deshalb darum, große Datenmengen auf mehrere Seiten aufzuteilen.
Dafür stellt der b4um Generator bereit:
bin/rails generate b4um:pagination Product
Wir werden dafür zunächst genügend Testdaten anlegen und anschließend zeigen, wie die Product-Übersicht serverseitig paginiert wird.
Besonders interessant wird dabei die Kombination mit unserer gerade eingebauten Suche:
Ein Suchbegriff soll auch beim Wechsel zwischen den einzelnen Seiten erhalten bleiben.

Meld dich an und schreibe ein Kommentar