Jedna aplikacja dla wielu serwisów

Chcę stworzyć aplikację, która będzie obsługiwać wiele niezależnych serwisów.
Każdy serwis ma własną bazę danych (identyczna struktura, różnią się zawartością), layout jednakowy ale mogą się różnić kolorystyką i grafikami (logo itp.), każdy serwis ma własny katalog “załączników” (pdf, doc itp.) Jak się za to zabrać?
Mam taką aplikację napisaną w PHP: na podstawie adresu określa właściwą bazę, nazwy katalogów ze “skórką” i “załącznikami”. Jak podobną funkcjonalność uzyskać w aplikacji RoR?

  1. Jedna baza.
  2. Nowy model określający aplikację.
  3. Wszystkie inne modele (może oprócz usera?) mają być przypisane do konkretnej aplikacji. To samo do załączników.
  4. Zabawa z uprawnieniami, autoryzacją itp.
  5. Jakiś fajny routing, np. po subdomenie, który będzie określał o jaką aplikację chodzi.

Dzięki za zainteresowanie ale takie rozwiązanie było rozważane i zostało odrzucone. Przyczyna: każdy serwis/domena to osobny klient i dane serwisu muszą być odseparowane od danych innych serwisów - wymóg “wyższej instancji”. Routing po subdomenie również odpada bo to nie będą subdomeny ale różne domeny podawane przez klienta w chwili uruchomienia danego serwisu.
Myślałem o tym aby mieć bazę danych zawierającą informację o serwisach (umożliwiłoby to zarządzanie serwisami z poziomu przeglądarki) i w trakcie wykonania dołączać drugą bazę danych właściwą dla danego serwisu. Ale jak to zrealizować aby aplikacja pracowała z dwoma bazami równocześnie?

[code]# w 1 bazie trzymani klienci z informacjami o ich bazach

domenach itp

class Client < ActiveRecord::Base
end

Abstrakcyjna klasa dla rzeczy które są u każdego

klienta trzymane w osobnej bazie

class ClientSpecific < ActiveRecord::Base
self.abstract_class = true
end

Np posty na bloga każdy klient ma swojej bazie.

class Post < ClientSpecific
end

class ApplicationController
before_filter do
@client = Client.find_by_domain(request.domain)

# Wszystkie modele dziedziczące z ClientSpecific niech w tym requeście korzystają z połączenia bazodanowego
# które określimy na podstawie danych klienta
ClientSpecific.establish_connectoin(@client.connection_data)  # http://api.rubyonrails.org/classes/ActiveRecord/Base.html#method-c-establish_connection

end
end

class PostController < ApplicationController
def show
@post = Post.find(params[:id])
end
end[/code]
Kod pisałem z głowy. Potraktuj to bardziej jako wskazówkę/pseudokod niż gotowe rozwiązanie. Trzeba by jeszcze do tego dodać w layoucie aplikacji by po odpowiedniej ścieżce wczytywał JS / CSS specyficzny dla klienta ale to już banał. Nie gwarantuję, że to rozwiązanie jest threadsafe.

Dzięki za wskazówkę. Mam tylko jedną wątpliwość bo nie znalazłem tego w kodzie: czy użycie metody establish_connectoin za każdym razem powoduje łączenie z bazą danych czy też wykonywane jest tylko w sytuacji gdy takie połączenie nie jest ustanowione?

Pozostaje problem asset-ów i nie jest to takie banalne. Powiedzmy, że w kodzie mamy image_tag “logo.png”. Jeśli w katalogu danego serwisu występuje taki plik to należy go wykorzystać, jeśli nie to wykorzystać plik app/assets/images/logo.png.
Kolejna spawa to struktura katalogów. Można oczywiście wykorzystywać strukturę:

app -assets -stylesheets -user_1 -user_2 -... -images -user_1 -user_2
Jednak praktyczniejsze by było:

app -users_dir -user_1 -stylesheets -images -user_2 -stylesheets -images
Można by było dodawać kolejne ścieżki do Rails.application.config.assets.paths ale … wszędzie będą pliki o tych samych nazwach więc wykorzystywany będzie pierwszy napotkany. Poza tym ścieżka raz dodana do listy ścieżek pozostanie tam tak długo jak długo działać będzie aplikacja. Trzeba raczej jakoś dynamicznie modyfikować te ścieżki. I co później z prekompilacją?
Można by było users_dir umieścić bezpośrednio w public/ ale jak zmusić helpery takie jak image_tag czy stylesheet_link_tag do szukania plików w takim katalogu (pomijając wpisywanie na “sztywno” ścieżek bo tracę w tym momencie “plik użytkownika a jak nie ma to domyślny”)
Jak takie rozwiązanie zrealizować?

Olej zupełnie koncepcję assetów i przekompilacji bo tylko ci utrudnia sprawę. Ja widzę to tak, że opierasz się tylko na stylach wszędzie. Zawsze linkujesz domyślne style a następnie zawsze linkujesz style danej firmy/klienta. Firma (albo admin) ma GUI, w którym może je edytować. Obrazki zawsze pokazuj z użyciem jakiś klas css, wtedy istnieje bezproblemowa możliwość ich nadpisania poprzez nadpisanie stylów. A style nadpiszesz edytując plik stylów danej firmy, który ponieważ będzie drugi to eee nadpisze :slight_smile: Ten plik stylów firmowych może na początku nie istnieć (przeglądarka przez to nie padnie) albo może być po prostu pusty (bardziej elegancki rozwiązanie).

layout:

stylesheet_link_tag "default" stylesheet_link_tag "#{@client.name}/style"
katalogi:

app/ public/ company1/ company1logo.png style.css # Nadpisuje np regułę cssową: .logo{ background-image: by wskazywał na company1/company1logo.png zamiast na obrazek domyślny } javascript.js # mozliwe ze nadpisywanie JS per firma nie bedzie konieczne. Staralbym sie zrobic wszystko by nie bylo :)

Ja bym zaczął od połączenia baz danych w jedno. To znaczy jeżeli mają identyczną strukturę, to czemu nie połączyć ich w jedno. Dodając po prostu do każdej tabelki pole client_id np?

Rozwiązanie panq’a ma bardzo mocną wadę - niestety dla każdego połączenia ustawia nowe połączenie z bazą danych :frowning:

Ja bym zrobił coś takiego:

class MultiConnectionBase < ActiveRecord::Base self.abstract_class = true cattr_accessor :client CONNECTION_POOL = {} def self.connection CONNECTION_POOL[client.id] ||= establish_connection(client.connection_data) end end
Dodatkowo upewnij się że ActiveRecord::Base.allow_concurency jest na pewno false.

Ważne jest też żeby upewnić się że to wszystko będzie hulać z twoim serwerem www. Passenger i/lub unicorn robią różne fajne rzeczy typu spawn/fork i mogą trochę namieszać.

Kolega już napisał, że nie może tego zrobić (“wymóg wyższej instancji”). Tak poza tym to nie jest to zła rada :slight_smile:

[quote=paneq]Olej zupełnie koncepcję assetów i przekompilacji bo tylko ci utrudnia sprawę. Ja widzę to tak, że opierasz się tylko na stylach wszędzie. Zawsze linkujesz domyślne style a następnie zawsze linkujesz style danej firmy/klienta. Firma (albo admin) ma GUI, w którym może je edytować. Obrazki zawsze pokazuj z użyciem jakiś klas css, wtedy istnieje bezproblemowa możliwość ich nadpisania poprzez nadpisanie stylów. A style nadpiszesz edytując plik stylów danej firmy, który ponieważ będzie drugi to eee nadpisze :slight_smile: Ten plik stylów firmowych może na początku nie istnieć (przeglądarka przez to nie padnie) albo może być po prostu pusty (bardziej elegancki rozwiązanie).

layout:

stylesheet_link_tag "default" stylesheet_link_tag "#{@client.name}/style"
katalogi:

app/ public/ company1/ company1logo.png style.css # Nadpisuje np regułę cssową: .logo{ background-image: by wskazywał na company1/company1logo.png zamiast na obrazek domyślny } javascript.js # mozliwe ze nadpisywanie JS per firma nie bedzie konieczne. Staralbym sie zrobic wszystko by nie bylo :)
[/quote]
Po przemyśleniu Twojej propozycji … faktycznie dam sobie spokój z asset-ami dla danych klientów i będę je umieszczał bezpośrednio w public.
Co do powyższego “kodu” to chyba popełniłeś mały błąd. Dla stylów i grafik podanych przez ścieżki względne wyszukiwanie będzie w app/public/assets. Ścieżki powinny być bezwzględne.

Co do grafik w css … jakoś nie podoba mi się takie rozwiązanie. Napiszę sobie helper my_image_tag i będzie dobrze.

Jeszcze raz dzięki za wskazówki.

Temat powrócił a ja ciągle nie wiem jak sobie z tym poradzić.
Pomysł z before_filter (teraz już before_action) jest bardzo dobry ale …
W RoR 5.1.4 mamy ApplicationRecord i establish_connection umieściłem właśnie w tej klasie. Przy pierwszym uruchomieniu serwera następuje połączenie do właściwej bazy danych. Jednak połączenia na inny adres nie powodują przełączenia do innej bazy danych. Śledząc wykonanie programu zauważyłem, że nazwa połączenia bazuje na nazwie klasy (w tym przypadku zawsze ApplicationRecord) a nie na nazwie konfiguracji. Może jest jakaś możliwość wymuszenia zamknięcia aktywnej puli połączeń i uruchomienia nowej?