V minulém díle jsem nakousl dvě funkce budoucích stránek, a to knihu hostů a blog. Nikde se zatím vazby mezi tabulkami nevyskytovaly, blog se choval stejně jako kniha hostů, jen s rozdílem v názvech modelů, kontrolérů a pohledů. Cílem bylo udělat prototyp stránek „klikatelný“.
Co bude dál?
Blog i knihu hostů zesložitíme. Jednak těch blogů bude více (registrovaní uživatelé jich budou mít i více), za druhé knihu hostů uděláme víceúrovňovou (povětví se, jak budou uživatelé odpovídat na zprávy).
V tomto a příštím díle dotáhneme blog do výsledné podoby. Na blogu si ukážeme použití jednoduchých vazeb mezi tabulkami. Poté přijde na řadu kniha hostů, kde se vazby také použijí, ale jinak – docílíme v ní stromové struktury.
Ostatní funkce stránek zatím nechám být. Přestože grafika stále není hotova, nevadí; je známa struktura stránek a grafiku „dolepíme“ třeba až na konci. Z analýzy vyplývá, že se v prezentaci vyskytnou jen asi čtyři typové stránky: seznam prvků, zobrazení jednoho prvku, galerie a zobrazení stromové struktury.
O nás | je stránka se statickým obsahem a nemá smysl se jí zabývat |
Novinky | jsou de facto stávající, jednoúrovňovou, knihou hostů |
Články | fungují v principu stejně jako stávající blog (jednoúrovňová kniha hostů) |
Blogy | Nejprve se zobrazí seznam blogů a až poté články konkrétního blogu (v témž zobrazení jako Články). |
Kniha hostů | bude narozdíl od stávající knihy víceúrovňová (stromová struktura) |
Diskuse | nejspíš seznam témat vedoucí do čehosi jako víceúrovňové knihy hostů |
Odkazy | á la jednoúrovňová, stávající kniha hostů |
Chat | totéž, jednoúrovňová, stávající kniha hostů s automatickým odmazáváním starších zpráv |
Galerie | Buď jako seznam vedoucí na seznam snímků nebo složitěji, jako univerzální stromová struktura složek, podsložek a snímků v nich. Možná by tak šly řešit i diskuse, čímž by mohly mít podkategorie. Ostatně, všechno je svým způsobem stromová struktura, o jejímž absolutním počátku lze jen diskutovat… |
Portréty | stránka zobrazující rozšířené údaje o uživateli a uživatelova galerie, případně jen odkaz do konkrétní galerie v Galeriích. |
Blog
Konečně k věci. Jaké vazby budou mezi tabulkami blogu, a vůbec, jakými tabulkami? Plánována je registrace uživatelů (tabulka uživatelé). Každý uživatel bude vlastnit jeden či více blogů (tabulka seznam blogů) a každý blog bude sestávat z článků (tabulka články blogu).
Příprava tabulek
Máme tři tabulky:
- uživatelé (login, heslo, datum vytvoření)
- seznam blogů (jméno blogu, popis blogu)
- články blogu (název článku, tělo článku)
V databázi problém vyřešíme následovně (stávající tabulku blogs znovuvytvoříme):
users.sql:
create table users (
id int unsigned auto_increment,
login varchar(30),
password varchar(30),
created_on datetime,
primary key (id)
)
blogs.sql:
drop table if exists blogs;
create table blogs (
id int unsigned auto_increment,
user_id int unsigned,
name VARCHAR(100),
description TEXT,
constraint fk_user foreign key (user_id) references users(id),
primary key (id)
)
blog_articles.sql:
create table blog_articles (
id int unsigned auto_increment,
blog_id int unsigned,
title VARCHAR(255),
body TEXT,
constraint fk_blog foreign key (blog_id) references blogs(id),
primary key (id)
)
Názvy tabulek jsou v množném čísle, cizí klíče v jednotném a s příponou id.
automatické vygenerování
Pomocí scaffold generátoru si necháme vygenerovat editory všech tří tabulek a pak upravíme jejich modely.
Předtím, než scaffold generátory pustíme, musejí tabulky existovat, aby se podle typů jejich sloupců předgenerovaly prvky formulářů.
mig> cat users.sql | mysql -u root --password=abcd rolldance_development
mig> cat blogs.sql | mysql -u root --password=abcd rolldance_development
mig> cat blog_articles.sql | mysql -u root --password=abcd rolldance_development
Spustíme scaffold generátory, přičemž nejprve odstraníme původní blog:
mig> script/destroy scaffold Blog
mig> script/generate scaffold User
mig> script/generate scaffold Blog
mig> script/generate scaffold BlogArticle
tip: Proč jsem použil jednotné číslo v názvech? Help pomůže. Spusťte scaffold generátor bez dalších parametrů (platí i pro ostatní generátory).
mig> script/generate scaffold
Usage: script/generate scaffold ModelName [ControllerName] [action, ...]
Generátor očekává jméno modelu jako název. Model je vždy v jednotném čísle a má první písmena velká (model BlogArticle se pak vyskytuje v souboru blog_article.rb)
Budiž server!
mig> script/server
nyní vyzkoušejte
- http://localhost:3000/blogs
- http://localhost:3000/users
- http://localhost:3000/blog_articles
Možná jste si všimli, že ve formulářích chybějí položky k zadání cizích klíčů. Jestliže vytvoříte nějaké záznamy a nahlédnete do databáze, ani tam nebudou klíče nastaveny:
mysql> select * from blogs;
+----+---------+-----------+-------------+
| id | user_id | name | description |
+----+---------+-----------+-------------+
| 1 | NULL | novy blog | blog |
+----+---------+-----------+-------------+
Scaffold generátor bohužel není dokonalý a neřeší vazby mezi tabulkami. Nevadí, máme jednoduché webové editory tabulek, které bychom jinak pracně psali, a co chtít víc, že?
ActiveRecord a mezitabulkové vazby
ActiveRecord musíme vazbám naučit, z popisu tabulek totiž automaticky nepozná, která tabulka je spřažena s kterou. Ne všechny databáze podporují vazby, ačkoli i s takovými ActiveRecord umí zacházet.
Editujme všechny tři modely:
app/models/user.rb
class User < ActiveRecord::Base
has_many :blogs
end
app/models/blog.rb
class Blog < ActiveRecord::Base
belongs_to :user
has_many :blog_articles
end
app/models/blog_article.rb
class BlogArticle < ActiveRecord::Base
belongs_to :blog
end
vytváření záznamů
Co znamená úprava modelů v praxi? V jednotlivých modelech (třídách) díky has_many a belongs_to dynamicky přibudou nové metody pro práci se spřaženými tabulkami. Není nic jednoduššího než spustit interaktivní konzoli a tam modely vyzkoušet.
mig> script/console
Loading development environment.
>> user = User.new
>> user.login = 'mat'
=> #<User:0x40858fe0 @new_record=true, @attributes={"created_on"=>nil, "password"=>nil, "login"=>'mat'}>
Co se stalo? Vytvořili jsme instanci modelu User, tedy objekt odpovídající řádku tabulky users. Podívejme se na ty jeho metody, které mají v názvu „blog“.
>> user.methods.grep /blog/
=> ["find_all_in_blogs", "create_in_blogs", "validate_associated_records_for_blogs", "build_to_blogs", "blog_ids=", "blogs_count", "add_blogs", "remove_blogs", "blogs", "blogs=", "has_blogs?", "find_in_blogs"]
Zajímavá je metoda create_in_blogs (pro přehlednost nebudu uvádět výsledky příkazů).
>> blog = user.create_in_blogs
>> blog.name = 'matuv blog'
>> blog.description = 'tohle je muj prvni pokusny blog'
Teď v prohlížečí otevřete tabulku tabulku blogů a porovnejte změny. Žádné by se neměly odehrát, dokud objekt neuložíme.
>> user.save
Tím došlo k uložení jak uživatele, tak blogu. Tabulky nyní vypadají takto:
mysql> select * from users;
+----+-------+----------+---------------------+
| id | login | password | created_on |
+----+-------+----------+---------------------+
| 3 | mat | NULL | 2005-12-01 03:11:47 |
+----+-------+----------+---------------------+
1 row in set (0,00 sec)
mysql> select * from blogs;
+----+---------+--------------+---------------------------------+
| id | user_id | name | description |
+----+---------+--------------+---------------------------------+
| 9 | 3 | matuv blog | tohle je muj prvni pokusny blog |
+----+---------+--------------+---------------------------------+
1 row in set (0,00 sec)
Kromě create_in_blogs lze použít build_to_blogs. Jaký je mezi nimi rozdíl?
>> blog_a = user.create_in_blogs( :name=>'matuv blog A', :description=>'druhy pokusny blog' )
>> blog_b = user.build_to_blogs( :name=>'matuv blog B', :description=>'treti pokusny blog' )
V prohlížeči zkontrolujte aktuální stav blogů. Měli byste vidět nový blog A, nikoli však B. Blog B spatříte v databázi až po jeho uložení, ať už takto
>> user.save
nebo přímo
>> blog_b.save
Rozdíl mezi create_in a build_to tedy spočívá v tom, že create volá současně i save, takže ihned po jeho volání řádek v tabulce existuje. Kdybyste ale toto zkoušeli ještě předtím, než byl uložen uživatel, obě metody by se chovaly stejně, poněvadž id uživatele by nebylo známo.
Create_in využijeme například tehdy, specifikujeme-li všechny parametry do závorky konstruktoru, kdežto build_to tehdy, vytvoříme-li nový objekt a teprve později nastavíme jeho parametry pomocí metod objektu. Při použití create_in bez specifikovaných parametrů v závorce se provede zbytečný insert, který vloží prázdný řádek, a teprve update (save) ho následně vyplní.
Přidáme dalšího uživatele mat a jemu jeden blog (konstrukce „vše v jednom“):
>> User.create(:login=>'pat').create_in_blogs(:name=>'blog druheho uzivatele').save
Abychom lépe porozuměli metodě has_many, zde jsou znovu obě tabulky
mysql> select * from users;
+----+-------+----------+---------------------+
| id | login | password | created_on |
+----+-------+----------+---------------------+
| 3 | mat | NULL | 2005-12-01 03:11:00 |
| 4 | pat | NULL | 2005-12-01 05:27:00 |
+----+-------+----------+---------------------+
2 rows in set (0,00 sec)
mysql> select * from blogs;
+----+---------+------------------------+---------------------------------+
| id | user_id | name | description |
+----+---------+------------------------+---------------------------------+
| 9 | 3 | matuv blog | tohle je muj prvni pokusny blog |
| 10 | 3 | matuv blog A | druhy pokusny blog |
| 11 | 3 | matuv blog B | treti pokusny blog |
| 12 | 4 | blog druheho uzivatele | NULL |
+----+---------+------------------------+---------------------------------+
4 rows in set (0,00 sec)
Has_many zřejmě funguje jako list.
čtení záznamů
Zkusme místo selectů objektový přístup. Vylistujme všechny uživatele:
>> User.find( :all )
Obdobně se získá seznam blogů:
>> Blog.find :all
či přímo první blog (výsledkem nebude pole):
>> Blog.find :first
Chceme-li vylistovat pouze blogy uživatele mat a známe jeho id (id=3), učiníme tak rovnou pomocí modelu Blog. Podmínky se zadávají stejně jako where část sql dotazu.
>> Blog.find( :all, :conditions=>'user_id=3' )
Totéž, neznáme-li uživatelovo id:
>> user = User.find( :first, :conditions=>[ 'login=?', 'mat' ] )
>> user.blogs
Podotýkám, že konstrukci [ ‚login=?‘, ‚mat‘ ] jsem uvedl, abych ošetřil případné nebezpečné znaky předtím, než se dostanou do sql dotazu. Otazníků lze uvést více, pak jim odpovídá také více položek pole, např. [ ‚login=? OR password=?‘, ‚mat‘, ‚abcd123‘ ].
Poněvadž má i model Blog v záhlaví has_many, bude se Blog chovat k BlogArticle obdobně jako User k Blogu. Článek v prvním blogu uživatele mat vyrobíme podobně, jako jsme vyrobili uživatelův blog. (pokračuji na konzoli)
>> blog = user.blogs[0]
>> blog.methods.grep /art/
=> ["blog_articles", "build_to_blog_articles", "blog_articles=", "blog_article_ids=", "blog_articles_count", "add_blog_articles", "remove_blog_articles", "has_blog_articles?", "find_in_blog_articles", "find_all_in_blog_articles", "create_in_blog_articles", "validate_associated_records_for_blog_articles"]
>> article = blog.build_to_blog_articles
>> article.title = 'dnes je streda, co bude asi zitra?'
>> article.body = 'Dnes je streda a linux je proste nejlepsi system a jste blbove kdyz pouzivate M$ shit a svitilo slunicko cely den.'
>> blog.save
Viz výsledek v prohlížeči ( http://localhost:3000/blog_articles ).
Zbývá objasnit metodu belongs_to. Díky tomu, že jsme ho uvedli v modelu BlogArticle, přidal do modelu tyto metody
>> article.methods.grep /blog/
=> ["blog", "blog=", "set_blog_target", "build_blog", "create_blog", "has_blog?", "blog?"]
Pomocí build_blog či create_blog tak můžeme vytvořit blog obráceným směrem, což je poněkud krkolomné. Později se nejspíš bude hodit metoda blog, abychom zjistili, do jakého blogu článek patří. Obdobně v modelu Blog, který má též uvedeno belongs_to, najdeme metody
>> Blog.new.methods.grep /user/
=> ["user", "user=", "set_user_target", "build_user", "create_user", "has_user?", "user?"]
Kdyby v modelu Blog belongs_to chybělo, nalezli bychom tam jen id uživatele vlastnícího blog
> > Blog.new.methods.grep /user/
=> ["user_id"]
další užitečné parametry
app/models/user.rb
class User < ActiveRecord::Base
has_many :blogs, :order=>'name DESC', :dependent=>true
end
app/models/blog.rb
class Blog < ActiveRecord::Base
belongs_to :user
has_many :blog_articles, :order=>'title DESC', :dependent=>true
end
Parametr order ovlivní pořadí, v jakém dostaneme seznam blogů čí článků blogu.
>> user.blogs
...
>> blog.blog_articles
...
Důležitým parametrem je dependent. Jestliže se v blogu nacházejí články a blog zrušíme,
>> blog.destroy
zruší se při nastaveném dependent i všechny jemu náležející články. Bez parametru dependent by články v tabulce blog_articles zůstaly a zrušen by byl jen záznam v tabulce blogs.
has_one, has_many, belongs_to a has_many_and_belongs_to v kostce
Když jsem začal pracovat s Rails, měl jsem trochu nepořádek v tom, co definice vazeb dělají. Shrňme si to:
typ vazby one-to-many
V případě, že k položce v rodičovské tabulce náleží více položek v dceřiné tabulce, použijeme has_many a belongs_to. To je případ našeho blogu, jde o list. V ActiveRecord je list představován polem objektů vzniknuvších z řádků dceřiné tabulky.
class Blog < ActiveRecord::Base
has_many :articles
end
class Article < ActiveRecord::Base
belongs_to :blog.
end
Jinými slovy, blog má mnoho článků, každý článek náleží jednomu blogu.
create table blogs (
id int unsigned auto_increment,
primary key (id)
)
create table articles (
id int unsigned auto_increment,
blog_id int unsigned,
constraint fk_blog foreign key (blog_id) references blogs(id),
primary key (id)
)
typ vazby one-to-one
Může nastat případ, kdy položka rodičovské tabulky může mít nejvýše jednu položku v dceřiné tabulce. Chování je podobné has_many, ale při „přidání“ další položky do dceřiné tabulky se stávající položce vynuluje cizí klíč, čímž se zruší vazba k rodičovské tabulce. Teprve pak se přidá položka nová, která bude s rodičovskou tabulkou provázána, takže rodičovská tabulka bude mít opět pouze jednu položku v dceřiné tabulce.
Přidané metody jsou podobné těm, které přidává belongs_to (z obou stran jde o vazbu jedna ku jedné). Jaký je rozdíl? Když uložíme objekt typu has_one, uloží se i vnořené objekty. Naopak uložíme-li objekt, jehož model má nastaveno belongs_to, uloží se pouze tento objekt. Pozor, kdybychom v dceřiném modelu nastavili místo belongs_to také has_one, byl by očekáván cizí klíč v rodičovské tabulce směřující do dceřiné.
class Blog < ActiveRecord::Base
has_one :articles
end
class Article < ActiveRecord::Base
belongs_to :blog.
end
Jinými slovy, blog má nejvýše jeden článek, každý článek náleží jednomu blogu.
create table blogs (
id int unsigned auto_increment,
primary key (id)
)
create table articles (
id int unsigned auto_increment,
blog_id int unsigned,
constraint fk_blog foreign key (blog_id) references blogs(id),
primary key (id)
)
typ vazby many-to-many
Představte si blok, jenž má více článků. Zároveň však jeden článek může být sdílen více blogy. Docílíme toho pomocí třetí, spojovací tabulky, jejíž název sestává z názvů obou tabulek v abecedním pořadí.
class Blog < ActiveRecord::Base
has_and_belongs_to_many :articles,
end
class Article < ActiveRecord::Base
has_and_belongs_to_many :blogs
end
create table blogs (
id int unsigned auto_increment,
primary key (id)
)
create table articles (
id int unsigned auto_increment,
primary key (id)
)
create table blogs_articles (
blog_id int unsigned,
article_id int unsigned,
constraint fk_blog foreign key (blog_id) references blogs(id),
constraint fk_article foreign key (article_id) references articles(id)
primary key (article_id, blog_id)
)
Závěr
Příště dokončíme blog, začneme použitím modelů v kontrolérech a pak k nim doděláme šablony, aby bylo konečně něco vidět.