Boolean attributes

Mam aplikację, która działa na bazie Oracla, chciał bym się do niej podpiąć RoR, żeby sobie troche danych powyciągać i dorobić API. Bazy zmienić nie mogę.
Problem jaki mam to mapowanie kolumn w bazie, które przechowują wartości ‘T/F’ na boolean.
Działa to u mnie połowicznie dodałem do modelu

attribute :nieuzywana, :boolean

i Active record zwraca mi poprawnie

>> OplatyStaleDefinicje.first.nieuzywana
  OplatyStaleDefinicje Load (9.5ms)  SELECT * FROM (SELECT "OPLATY_STALE_DEF".* FROM "OPLATY_STALE_DEF" ORDER BY "OPLATY_STALE_DEF"."ID_OPLATY_STALE_DEF" ASC ) WHERE ROWNUM <= :a1  [["LIMIT", 1]]
=> false

Natomiast kiedy próbuje użyć tego w where to, true/false mapowane jest na wartości 0/1 zamiast używane w bazie ‘T/F’

>> OplatyStaleDefinicje.where nieuzywana: false
=> []
  SELECT "OPLATY_STALE_DEF".* FROM "OPLATY_STALE_DEF" WHERE "OPLATY_STALE_DEF"."NIEUZYWANA" = :a1  [["nieuzywana", "0"]]

W tym momencie nawet podanie w where wartośći ‘F’ jest mapowane na 0

>> OplatyStaleDefinicje.where nieuzywana: 'F'
=> []
OplatyStaleDefinicje Load (15.0ms)  SELECT "OPLATY_STALE_DEF".* FROM "OPLATY_STALE_DEF" WHERE "OPLATY_STALE_DEF"."NIEUZYWANA" = :a1  [["nieuzywana", "0"]]

Jest jakiś sposób, żeby to skonfigurować?

Szybkie Google pokazuje, że “The Oracle RDBMS does not support a Boolean datatype. You can create a table with a column of datatype CHAR(1) and store either “Y” or “N” in that column to indicate TRUE or FALSE […]” Nie wiem na ile to aktualne, ale w takiej sytuacji zaczął bym od sprawdzenia jakiego faktycznie typu jest ta kolumna i jakie są tam wartości.

VARCHAR ale przechowuje dane nie jako znaki ‘Y’ i ‘N’, a ‘T’ i ‘F’. Na razie zrobiłem to tak, że w inicializers dodałem

ActiveRecord::ConnectionAdapters::OracleEnhanced::Quoting.class_eval do

    def quoted_true
      return "'T'" if emulate_booleans_from_strings
      "1"
    end

    def unquoted_true
      return "T" if emulate_booleans_from_strings
      "1"
    end

    def quoted_false
      return "'F'" if emulate_booleans_from_strings
      "0"
    end

    def unquoted_false
      return "F" if emulate_booleans_from_strings
      "0"
    end

  end

Domyślnie było tam właśnie Y/N i po tych zmianach nawet działa.
Zastanawiam się tylko, czy nie da się tego jakoś bardziej elegancko skonfigurować.

Pytanie czy jest jakiś nadrzędny powód, żeby w ogóle to mapowanie robić? Zwykle staram się unikać wartości boolean mapowanych z baz, jako że to często prowadzi do mało czytelnych błędów. Łatwo doprowadzić do sytuacji, gdzie NULL/nil będzie błędnie interpretowany jako false. Radziłbym dodać sobie po prostu enum do modelu, zmapować te wartości T/N na coś bardziej czytelnego/opisowego enabled/disabled (?) i nie używać nigdzie true/false. Tym bardziej unikałbym monkey patch w okolicach ConnectionAdapter

A w jaki sposób zrobić takie mapowanie w enum?

Miałem na myśli zwykły, ActiveRecord::Enum w modelu, żeby móc używać scope w stylu Model.active zamiast where. Czyli coś ala:

class OplatyStaleDefinicje < ApplicationRecord
  enum nieuzywana: {
    active: "N",
    disabled: "T",
  }
end

Tu przy okazji wychodzi trochę drobiazgów, które mogą być mniejszym czy większym problemem. Ogólnie lepiej unikać polskich nazw klas, zmiennych. Nawet kiedy nie ma bezpośredniej kontroli nad danymi, to mimo wszystko użyłbym angielskich nazw. Tutaj i tak musisz podać nazwę tabeli w modelu, ze względu na pluralizację nazwy. Atrybut można zmienić na klika sposobów, np: alias_attribute.

Drugi problem to negacja flagi. Jak rozumiem TRUE w polu nieuzywana oznacza, ze ta definicja nie jest używana? O niebo lepsze byłoby było tam po prostu active: T/F. Musisz podjąć decyzję, czy mapować to tak jak jest i dalej męczyć się z tą formą, czy odwrócić i trzymać u siebie po prostu active/disabled.

Zastanawiałem się mocno nad tym, ale niewiem czy dobrym pomysłem jest wymyślanie nowej nomenklatury jeśli jest ona już w tej bazie i tak jest używana w innej aplikacji i raportach, które są z niej generowane. Samo używanie polskich nazw mi się też nie podoba.

1 Like