Witam w przedostatniej (nielicząć części VII która dot. "przybajerzenia" naszego czatu ;) ) części tutoriala, poświęconego budowaniu chatu z pomocą Railsów oraz Juggernaut.

W tej części zajmiemy się połączeniem użytkowników z pokojami, tak aby w ostatniej części tutoriala, można było na bieżąco aktualizować spis użytkowników z danych pokoi.

Aby dokonać takiego połączenia które jest połączeniem wiele-do-wielu (wielu użytkowników może być w wielu pokojach ale i wiele pokoi może zawierać wielu użytkowników) można skorzystać z dwóch sposobów:

  1. Możemy skorzystać z has_and_belongs_to tworząc tabelę przechodnią bez tworzenia dla niej modelu
  2. Skorzystać z has_many :through i stworzyć dla tej relacji model

My skorzystamy z metody drugiej, chociaż na pierwszy rzut oka rozwiązanie pierwsze wydaje się lepsze.

Dlaczego więc nie jest?

Kiedy pisałem ten czat, sam skorzystałem z pierwszej metody, jednak Juggernaut posiada system callbacków przy których praca z has_and_belongs_to robiła się troszkę problematyczna. Kiedy ktoś korzystał z jednego okna czatu naraz wszystko było ok, jednak kiedy powiedzmy z czystej ciekawości, ktoś w drugiej karcie otwierał drugi raz ten sam pokój, a następnie zamykał jedną kartę, relacja has_and_belongs_to była usuwana (ponieważ callback miał usuwać relację aby lista była aktualna). W takiej sytuacji użytkownik był na czacie jednak znikał z listy użytkowników (system uznawał zamknięcie karty za zamknięcie czatu).

Jak to rozwiązałem?

Bardzo prosto. Stworzyłem model ChatRoomsUser którego zadaniem było stworzenie relacji między użytkownikami i pokojami, z jednoczesnym zliczeniem tego w ilu kartach ktoś "siedzi" w jednym pokoju czatu, tak więc kiedy wchodziliśmy pierwszy raz na czat, ilość naszych "istnień" w czacie była równa 1. Kiedy otwieraliśmy drugą zakładkę z tym samym pokojem, ilość wzrastała do 2.

Ten sam mechanizm działa w drugą stronę. Zamykając jedną zakładkę, Juggernaut zwraca nam callback że socket usera o takim a takim ID został zamknięty. Model obniża wtedy ilość o 1. Jeśli ilość była większa niż 1 (np 2) tzn że wciąż mamy aktywne przynajmniej jedno okno z pokojem. Oznacza to że wciąż jesteśmy w tym pokoju. Jednak kiedy zamykamy ostatnie okno, relacja zostaje usunięta i osoba znika z listy użytkowników tego pokoju.

Implementujmy

Implementację zaczniemy od utworzenia modelu którego migracja wygląda tak:

class CreateChatRoomUsers < ActiveRecord::Migration
  def self.up
    create_table :chat_rooms_users do |t|
      t.integer :user_id
      t.integer :chat_room_id
      t.integer :attemps

      t.timestamps
    end
  end

  def self.down
    drop_table :chat_rooms_users
  end
end

Sam model posiada informacje na temat tego do kogo należy. Posiada także dwie metody które odpowiadają za robienie tego co opisałem powyżej. Obie są metodami klasowymi ponieważ nie potrzebujemy tworzyć obiektu aby z nich korzystać.

Metoda connect - odpowiada za utworzenie relacji między userem i pokojem. Sprawdzamy najpierw czy istnieje już taka relacja (co oznacza że ktoś otwiera drugą kartę z tym samym pokojem) i jeśli tak, to podnosimy o 1 wartość pola attemps. Jeśli takiej relacji nie było (otwarcie pokoju po raz 1) to tworzymy taką relację.

Metoda disconnect - działa odwrotnie do poprzedniej. Zamykając pokój obniżamy wartość attemps o 1, chyba że ta była równa 1, wtedy usuwamy relację (ponieważ nie ma nas już w pokoju)

A oto kod:

class ChatRoomsUser < ActiveRecord::Base
  belongs_to :user
  belongs_to :chat_room

  def self.connect(user, room)
    el = self.find_by_user_id_and_chat_room_id(user.id, room.id)
    if el.nil?
      create(:user_id => user.id, :chat_room_id => room.id, :attemps => 1)
    else
      el.attemps += 1
      el.save
    end
  end

  def self.disconnect(user, room)
    el = self.find_by_user_id_and_chat_room_id(user.id, room.id)
    if el.attemps == 1
      el.delete
    else
      el.attemps-=1
      el.save
    end
  end

end

Aby wszystko działało należycie, do modelu ChatRoom musimy dodać opis naszej relacji:


has_many :chat_rooms_users
 has_many :users, :through => :chat_rooms_users

Dzięki temu możemy korzystać z metod w następujący sposób:


room = ChatRoom.find(params[:channels].first.to_i)
 user = User.find(params[:client_id])

 ChatRoomsUser.connect(user, room)

room = ChatRoom.find(params[:channels].first.to_i)
 user = User.find(params[:client_id])
 ChatRoomsUser.disconnect(user, room)

Części tutorialu:

  1. Juggernaut Rails Chat – część I (czym jest Juggernaut i jak go zainstalować)
  2. Juggernaut Rails Chat – część II (design)
  3. Juggernaut Rails Chat – część III (rejestracja i logowanie)
  4. Juggernaut Rails Chat – część IV (zarządzanie pokojami)
  5. Juggernaut Rails Chat – część V (łączenie użytkowników z pokojami)
  6. Juggernaut Rails Chat – część VI (odpalamy Juggernaut i nasz chat)
  7. Juggernaut Rails Chat – część VII (emotikonki i dźwięk)