W tym artykule przyjrzymy się projektowaniu modeli w prostej aplikacji. Przedstawię tutaj podstawowe relacje między nimi oraz komendy, dzięki którym można wygenerować modele z gotowymi migracjami do wykonania.
Przyjmijmy, że mamy aplikację z trzema modelami: Map, Tag, Marker oraz User.
User może dodawać wiele Map, Mapa posiada wiele Tagów oraz wiele Markerów. Jeden Tag może być przypisany do wielu Map. Marker należy tylko do jednej mapy. Kiedy usuniemy mapę, chcemy by markery, które zostały do niej dodane również zostały usunięte.
Na podstawie tego opisu klarują nam się następujące relacje:
class User
has_many :maps
end
class Map
belongs_to :user
has_many :markers, dependent: :destroy
has_and_belongs_to_many :tags
end
class Tag
has_and_belongs_to_many :maps
end
class Marker
belongs_to :map
end
Aby nie pisać wszystkiego ręcznie, możemy wykorzystać railsową komendę rails generate, by wygenerować modele i ich pola wraz z relacjami zawartymi między nimi w migracjach, które trzeba wykonać poprzez komendę rails db:migrate. Porządana struktura:
- User: name, email,
- Map: name, description,
- Marker: name, description, lat, lng,
- Tag: name
Dodatkowo, by relacja has_and_belongs_to_many między tagami, a mapami miała rację bytu, musimy wygenerować migrację z dodatkową tabelą zawierającą kolumny tag_id oraz map_id.
Generowanie modelu User (w tym przypadku nie zawieramy żadnych relacji, musimy dodać je ręcznie):
rails g model User name:string email:string
Migracja, którą otrzymaliśmy: db/migrate/__timestamp__create_users.rb:
class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :name
t.timestamps
end
end
end
a w pliku app/models/user.rb dodajemy linię has_many :maps
class User < ApplicationRecord
has_many :maps
end
Mapy:
rails g model Map name:string description:text user:references
Pole description definiujemy za pomocą identyfikatora text, by mieć możliwość wpisywania dłuższych ciągów znaków niż w name. Dzięki user:references, do tabeli maps zostanie dodana kolumna user_id, na podstawie której będziemy mogli sprawdzić, który użytkownik stworzył daną mapę.
Powyższa komenda generuje nam model Map z dodaną relacją belongs_to :user, dodajmy jeszcze pozostałe:
class Map < ApplicationRecord
belongs_to :user
has_many :markers, dependent: :destroy
has_and_belongs_to_many :tags
end
Tagi:
rails g model Tag name:string
modyfikujemy, by zawierały relację z mapami.
class Tag < ApplicationRecord
has_and_belongs_to_many :maps
end
Wspomniana dodatkowa tabela na połączenie Map z Tagami:
rails g migration CreateJoinTableMapTag maps tags
w wyniku której otrzymujemy następującą migrację:
class CreateJoinTableMapTag < ActiveRecord::Migration[5.2]
def change
create_join_table :maps, :tags do |t|
t.index [:map_id, :tag_id]
end
end
end
Markery:
rails g model Marker name:string description:text lat:float lng:float map:references
Tutaj pola lat i lng są typem Float. Tzn. liczbą zmiennoprzecinkową, ponieważ współrzędne geograficzne często mają rozwinięcie do kilku liczb po przecinku. Np. współrzędne Krakowa:
lat: 50.0646501, lng: 19.9449799.
map:references oczywiście dodaje nam relację belongs_to :map (zwróć uwagę na liczbę pojedynczą w map - ponieważ należy do jednej i tylko jednej mapy).
W tym momencie mamy gotowe modele oraz migracje. Czas na komendę rails db:migrate!
Po migracji możemy uruchomić railsową konsolę i sprawdzić, czy wszystko działa. Sprawdź, co zwraca konsola, kiedy wykonasz poniższe kroki:
$ rails console
irb 1> user = User.new(name: 'Joe')
irb 2> user.save
irb 3> user.maps.create(name: 'Map1', description: 'My favourite places in Cracow')
irb 4> map = user.maps.first
irb 5> map.tags.create(name: 'cities')
irb 6> map.markers.create(name: 'Cracow', lat: 50.0646501, lng: 19.9449799, description: 'The center of Cracow')
irb 7> map.markers.count
irb 8> user.maps.create(name: 'Second map', description: 'My favourite places to camp')
irb 9> map2 = user.maps.find(2)
irb 10> map2.tags << map.tags.first
irb 11> map.destroy
irb 12> Marker.all
Jak widać, nasze założenia zostały spełnione:
- nowo stworzony
User(1, 2) może dodawaćMapy (3), Mapa posiadaTagi(5) orazMarkery(6, 7),Tagmoże zostać dodany do więcej niż jednejMapy(8, 9, 10),Markernależy tylko do jednej mapy. Jeśli wykonamy instrukcje z poprzedniego punktu, jednak zamiastTagużyjemyMarker, marker dodany do pierwszej mapy zostanie przeniesiony do drugiej,Markery dodane do mapy zostają usunięte wraz zMapą (11, 12).
ActiveRecord Associations
W Railsach mamy więcej relacji, niż te, które zostały tu wymienione, a mianowicie:
- belongs_to,
- has_one,
- has_many,
- has_and_belongs_to_many,
- has_many :through,
- has_one :through,
Oraz polymorphic associations oraz self joins.
Jest ich tyle, że nie sposób opisać ich w jednym artykule. Polecam rails guides - ActiveRecord associations, a także rails guides - ActiveRecord migrations, z których wielokrotnie korzystałem pisząc powyższy tekst :)
Pozdrawiam,
R