• בלוג
  • באג ריפורט: מחיקה מרובה וחיפוש

באג ריפורט: מחיקה מרובה וחיפוש

02/04/2021

פעמיים בתקופה האחרונה נתקלתי באותו באג בשתי מערכות שונות ובשני המקרים עם תוצאה הרסנית, וזה לבד מצדיק פוסט קטן בתקווה שיעזור גם לכן להימנע מבעיות דומות.

1. מה קרה

במסך רשימת פריטים במערכת היתה לנו תיבת חיפוש וגם אפשרות לביצוע פעולה (למשל מחיקה) על כל הפריטים. בעזרת תיבת החיפוש משתמשים יכלו לקבל רק פריטים שמתאימים למילת חיפוש מסוימת. ברגע שמשתמש סינן חלק מהפריטים בתיבת החיפוש ואז לחץ "בחר הכל" לצורך ביצוע פעולה על כל הפריטים בתוצאות החיפוש, המערכת "שכחה" שאנחנו בתוך חיפוש וביצעה את הפעולה על כל הפריטים.

2. למה זה קרה

בשביל לממש ביצוע פעולה על מספר פריטים מרשימה, אם כל הפריטים מופיעים בקלאיינט, אפשר לשלוח לשרת את רשימת כל הפריטים (או מזהי הפריטים) עליהם רוצים לבצע את הפעולה. במקרה שלנו, ובדומה לממשק בג'ימייל ובמערכות נוספות, הרשימה כוללת המון המון פריטים מחולקים לעמודים. אפשרות "בחר הכל" אמורה לסמן את כל הפריטים מכל הדפים, כולל אלה שעכשיו אין עליהם מידע בקלאיינט.

הפיתרון שנבחר היה לשלוח פרמטר מיוחד שמציין שהפעולה שאנחנו מבצעים צריכה לרוץ על כל הפריטים כולל אלה שהקלאיינט לא רואה כרגע כי הם נמצאים בדפים אחרים.

הבעיה שלא היה תיאום בין פרמטר זה לבין פרמטר החיפוש: בקשה אחת סיננה את הפריטים לפי מילת חיפוש מסוימת, ובקשה אחריה ביצעה "מחיקה מרובה" ומחקה את כל הפריטים, בלי שהשרת יוכל להבין ש"כל הפריטים" אמורים היו להיות רק "כל הפריטים שמתאימים לחיפוש".

3. איך כדאי למנוע בעיות דומות בעתיד

המקור האמיתי לבעיה הוא מימוש לא נכון של מנגנון החיפוש. אם ניקח דוגמה מ Rails אפשר לדמיין קונטרולר בשם ItemsController עם פעולת index שמציגה את כל הפריטים ומטפלת בחיפוש:

class ItemsController < ApplicationController
    def index
        @items = Item.where(owner: current_user)
        if params[:search]
            @items = @items.where("name like '%?%'", params[:search])
        end
    end
end

ופה הבעיה - חיפוש הוא לא "עוד דרך" להציג תוצאות אלא הוא שינוי Context, הוא ממש משפיע על כל הפעולות על המסך שעכשיו צריכות להיות מושפעות מרשימת התוצאות.

גישה טובה יותר לכתיבת קוד החיפוש היא לבנות אותו בתור Controller נפרד עם פעולת index משלו:

class ItemsController < ApplicationController
    def index
        @items = Item.where(owner: current_user)
    end
end

class ItemsSearchController < ApplicationController
    def index
        @items = Item.where(owner: current_user).where("name like '%?%'", params[:search])
    end
end

בגישה כזאת כל הכפתורים ב View יכולים לשלוח אותנו ל Actions מתאימים ב Controller שלהם, ואז כפתור של Delete All היה מגיע ל ItemsSearchController#delete_all וזורק שגיאה כי הפעולה לא מומשה עדיין, במקום למחוק את כל הפריטים בלי קשר לתוצאות החיפוש.