Hlavní navigace

Ruby on Rails: Blog poprvé

2. 12. 2005
Doba čtení: 12 minut

Sdílet

Kromě toho, že ActiveRecord nahlíží na databázové tabulky objektově, zná i způsob, jak objektově pracovat s vazbami mezi nimi. Vodítko: has_many, has_one a belongs_to.

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.

Schématický obsah prezentace
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:

  1. uživatelé (login, heslo, datum vytvoření)
  2. seznam blogů (jméno blogu, popis blogu)
  3. č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://localhos­t:3000/blogs
  • http://localhos­t:3000/users
  • http://localhos­t:3000/blog_ar­ticles

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_ar­ticle.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://localhos­t:3000/blog_ar­ticles ).

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_be­longs_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.

CS24_early

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.

Byl pro vás článek přínosný?