Pisałem niedawno, że rozważam wciągnięcie RethinkDB do stosu technologicznego mojego projektu. To już się stało. Niemniej wcześniej, żeby ocenić jak bardzo różniłaby się praca z jedną i drugą bazą, odpaliłem na boku dodatkowy mini-projekt oparty o PostgreSQL.

Wspominałem, że od dawien dawna PostgreSQL był dla mnie "domyślną" bazą danych. Działo się tak z wielu przyczyn, a obecnie moje przekonanie o tym jest dużo większe, niż kiedyś. Ba, obecnie jestem w stanie spokojnie powiedzieć, że Postgres jest w stanie poradzić sobie lepiej w roli bazy typu NoSQL niż niektóre popularne silniki.

Ale do rzeczy. W moim mini-projekcie chciałem przechowywać nazwy użytkowników, którzy głosują za lub przeciw jakiemuś pomysłowi, jako zbiór. Jest to w wielu przypadkach równie efektywne, a nawet lepsze niż znane nam sztuczne tabele łączące relacji wiele do wielu, a poza tym czyni projekt znacznie prostszych. PostgreSQL, w przeciwieństwie do RethinkDB, nie ma wsparcia dla zborów, ma jednak strukturę tablicy i to ją zamierzałem wykorzystać.

Spodziewałem się jednak, że na drodze stanie mi Ecto – projekt dość młody, a więc być może nie wspierający takich hipsteriad. Wszak Railsom dodanie takiej fukncji zajęło dużo czasu, a nawet potem przed długo była ona pełna bugów. Byłem bardzo mile zaskoczony, kiedy okazało się, że zupełnie się mylę.

W kroku pierwszym musiałem dodać odpowiednie kolumny do mojej tabeli. Okazało się to być dziecinnie proste:

defmodule MyApp.Repo.Migrations.AddDownvotersAndUpvoters do
  use Ecto.Migration

  def change do
    alter table(:items) do
      add :upvoters, {:array, :string}, default: []
      add :downvoters, {:array, :string}, default: []
    end
  end
end

Prawda, jakie wygodne? W ten sposób utworzyłem moje arraye stringów i mogłem zacząć ich używać. Jak? Po prostu jak zwyczajnym elixirowych list, a Ecto pod spodem elegancko zapisywał wszystko w Postgresie. Zatem by uzyskać efekt zbioru (musiałem to uczynić po stronie aplikacji, jednak nie jest to duża strata):

def upvote(conn, %{"id" => id}) do
  user = conn.assigns[:current_user]
  item = Repo.get!(Item, id)
  downvoters = item.downvoters |> List.delete(user)
  upvoters = item.upvoters ++ [user] |> Enum.uniq
  changeset = Item.changeset(item, %{downvotes: downvoters, upvoters: upvoters})

  case Repo.update(changeset) do
    {:ok, item} ->
      conn
      |> redirect(to: item_path(conn, :index))
  end
end

Akcja dla downvote jest oczywiście bardzo podobna, więc wklejał nie będę.

Lekcja na dziś

... będzie bardzo prosta. Ecto jest naprawdę fajnie pomyślanym frameworkiem, który jak na razie robił zawsze dokładnie tego, czego chciałem.

Lekcja druga: Nie używajcie MongoDB, bo PostgreSQL i tak jest lepszy ;)