Chciałbym, żeby mi ktoś zaoszczędził 2 dni z mojego życia. Jeden z weekendów mógłby być o 2 dni dłuższy. Wyjazd na deskę mógłby trwać tyle ile by się chciało. Byłoby więcej czasu na czytanie książek, spędzanie czasu z bliskimi itp.
Ten “ktoś” jednak postanowił mi te 2 dni zabrać zamiast sprezentować… A tym “kimś” jest sposób interpretacji kolumny boolean przez Ruby on Rails.
Nie ma co jednak wybiegać do przodu, więc wszystko po kolei.
We wszystkich swoich dotychczasowych projektach używałem bazy MySQL, więc na tym będę bazował swoje rozważania.
Tworzę więc sobie model Person i daję mu atrybut friend, który może być true albo false. W migracjach, zgodnie z konwencją Rails, w modelu Person przy polu :friend daję typ :boolean. Wszystko na razie idzie zgodnie z planem. Po wykonaniu rake:db pojawia się tabela “people” i w niej pole ‘friend’ ma typ tinyint(1). Tu w sumie też wszystko idzie z planem, gdyż kolumna o typie tinyint(1) przez ActiveRecord rozpoznawana jest jako ‘boolean’.
MySQL ciągle się nie dopracował prawdziwego typu ‘boolean’, więc nawet w natywnym MySQL gdy się tworzy kolumnę typu BOOLEAN, to jest to równoznaczne ze stworzeniem kolumny o typie tinyint(1). Wartość zerowa jest traktowana jako false, a każda inna wartość jako true. (MySQL 5.0 Reference Manual - Overview of Numeric Types)
No i gdybym nie był “sprytniejszy”, niż trzeba, albo może gdybym kodował to nie będąc w pełni sił umysłowych, to nie byłoby żadnego problemu. Będąc jednak pomysłowym Dobromirem spojrzałem w strukturę bazy danych, zauważyłem typ tinyint(1), doszedłem do wniosku, że ani true ani false w tinyint się nie zmieści, więc będą tam tylko zera lub jedynki. I dalej szczęśliwie wrzucałem sobie do kodu tu i ówdzie takie oto kwiatki:
if Person.friend == 1 ... if Person.friend == 0 ...
W kodzie miałem oczywiście zmienne, a nie nazwy modeli, ale podaję dla zobrazowania. W każdym bądź razie te kawałki aż prosiły się o poprawienie, ale nów umysłowy (antonim pełni) był silniejszy i z zadowoleniem pisałem sobie dalej.
Można łatwo sobie dopowiedzieć czym to się skończyło. Absolutnie bezsensownymi dwoma dniami, gdzie aplikacja “dziwnie się zachowywała”.
Jeśli więc ktoś kiedyś również będzie pisał kod nie wkładając w niego za wiele myśli (z braku sił czy też jakiegokolwiek innego powodu), to chciałbym, żeby poniższe przykłady zapadły dostatecznie w pamięć, żeby napisać to dobrze. Jak się na to patrzy, to wszystko to powinno być proste i logiczne, ale rzeczy proste i logiczne daleko nie zawsze są proste.
Znajdujemy więc sobie dwie osoby, jedną która jest moim przyjacielem, a druga - niekoniecznie.
t = Person.find(:first, :conditions => ["..."]) # atrybut friend o typie tinyint(1) ma wartość 1 f = Person.find(:first, :conditions => ["..."]) # atrybut friend o typie tinyint(1) ma wartość 0
Sprawdzamy czy te osoby naprawdę są przyjaciółmi czy też nie.
t.friend # true f.friend # false t.friend? # true f.friend? # false
I właśnie sposób powyższy (najlepiej ze znakiem zapytania, bo jest bardziej czytelny i jasny w przekazie) jest prawidłowym i najlepszym sposobem na testowanie wartości boolean w Rails. Prawda, że proste?
Warto jednak rozpatrzyć jeszcze kilka przykładów, które czasami dają nam rezultaty, których oczekujemy, a czasem nas zaskakują. I to w niemiły sposób.
t.friend.blank? # false f.friend.blank? # true t.friend.empty? # NoMethodError: undefined method `empty?' for true:TrueClass f.friend.empty? # NoMethodError: undefined method `empty?' for false:FalseClass
Tutaj blank? działa, natomiast empty? odmawia współpracy. Jednak mimo iż blank? działa poprawnie, to jest to raczej niefortunny i nieklarowny zapis, więc lepiej go unikać.
Poniżej zaś to, co próbowałem robić w swoim kodzie. Dziecinny błąd. Children, don’t try this at home! :)
t.friend == 0 # false t.friend == 1 # false t.friend == "0" # false t.friend == "1" # false t.friend.to_i # NoMethodError: undefined method `to_i' for true:TrueClass f.friend.to_i # NoMethodError: undefined method `to_i' for false:FalseClass
Jak widać ani porównywanie tego do wielkości integer, ani string nie daje oczekiwanych rezultatów. Nawet próba konwersji wyrzuca błąd. Wniosek: nigdy nie porównywać boolean do wielkości liczbowych (i konkretnie do zer i jedynek).
Można porównywać je do wartości true/false (ale koniecznie nie jako string), ale to też jest mało eleganckie.
t.friend == true # true f.friend == true # false t.friend == "true" # false f.friend == "true" # false t.friend == "false" # false f.friend == "false" # false t.friend.to_s # "true" f.friend.to_s # "false"
Jeśli więc ktoś kiedyś dzięki powyższemu użyje formy prawidłowej zamiast niepoprawnej i zaoszczędzi przynajmniej kilka godzin (nie mówiąc już o dwóch dniach), to… przynajmniej nie będzie musiał pisać postów rozpoczynających się słowami “Chciałbym, żeby mi ktoś zaoszczędził 2 dni z mojego życia”.
Pewnego słonecznego dnia szedł sobie stary i mądry rabin przez pola i łąki. Rozmyślał nad Torą, nad niebezpiecznie rozluźniającymi się obyczajami w swojej gminie (młodzi już nie kłaniają się starszym tak nisko jak drzewiej bywało) i nad nadmiernie wzrastającym autorytetem rabina z sąsiedniej gminy za rzeką. Będąc dotychczas najbardziej szanowaną osobistością w całej okolicy między rzeką a tą miejscowością, dokąd czasem się zapuszczał dokonać bardziej nietypowych zakupów, niż chleb czy mięsiwo (np. kałamarze czy płótno), oczywistością było, że rosnący u boku autorytet jest co najmniej powodem do niespokojnego snu w nocy i podczas siesty. Idąc więc przez łąkę i bezwiednie muskając dłonią czubki wyższych traw, podświadomie poszukiwał “znaku”, który by go wewnętrznie umocnił w przekonaniu, że jego pozycja jest niezagrożona i że nieoczekiwane drgawki nie będą go wyrywały z błogiego stanu siesty.