RDR3T > Ассоциации и отношения между моделями. Валидация.
апреля 14, 2011 | Published in Ruby on Rails, Ruby on Rails 3 | 17 Comments
Я немного изменил планы и в этой статье не будет рассматриваться работа с RSpec, зато написанию спецификаций будет посвящена целая отдельная статья.
Итак, у нас имеется модель User, которая представляет собой пользователя нашего приложения. В данной статье мы создадим модель Post и контроллер PostsController, которые позволят нам ввести в приложение новую сущность и новый ресурс Post. Post представляет собой публикацию в блоге.
Поскольку любая публикация в блоге должна публиковаться от имени определенного пользователя, то нам необходимо определить связь (ассоциацию) между двумя моделями. Поскольку пользователь может иметь несколько публикаций, а публикация только одного автора, то нам следует установить отношение между моделями: User имеет много Post‘ов. Но прежде, давайте создадим модель и контроллер для работы с постами, а для этого создадим специальную ветку в нашем проекте и будем работать в ней:
$ git branch posts
Теперь если вы воспользуетесь командой, то увидите следущий ответ:
$ git branch
* master
posts
Звездочкой отмечена текущая ветка, для переключения на ветку posts следует воспользоваться командой:
$ git checkout posts
Switched to branch ‘posts’
Вы также могли бы создать ветку сразу из git checkout:
$ git checkout -b posts
Теперь, когда ветка posts создана и мы на нее переключились, мы можем заняться разработкой модели, контролера и представлений поста. В прошлой статье я обещал рассказать вам как создавать модели, контроллеры и другие компоненты приложения при помощи других встроенных в Rails генераторов, кроме scaffold. Сразу скажу, что в данной статье мы будем использовать стандартный генератор scaffold так как код генерируемый им нам весьма подходит, хотя код контроллера мы слегка упростим, чтобы он был идентичен коду из контроллера UsersController.
Для отдельной генерации моделей, контроллеров и так далее следует использовать команды:
rails g controller Posts
rails g model Post title:string text:text user_id:integer
rails g migration create_posts
Вместо всего этого мы, для упрощения работы, воспользуемся генератором scaffold:
rails g scaffold Post title:string content:text user_id:integer
Теперь, давайте отредактируем код контроллера так, чтобы он имел следущий вид:
class PostsController < ApplicationController def index @posts = Post.all end def show @post = Post.find(params[:id]) end def new @post = Post.new end def edit @post = Post.find(params[:id]) end def create @post = Post.new(params[:post]) @post.save ? redirect_to(@post, :notice => 'Post was successfully created.') : render(:action => "new") end def update @post = Post.find(params[:id]) @post.update_attributes(params[:post]) ? redirect_to(@post, :notice => 'Post was successfully updated.') : (render :action => "edit") end def destroy @post = Post.find(params[:id]) @post.destroy and redirect_to(posts_url) end end
Теперь, разобравшись со второстепенными заданиями давайте перейдем к самой сути главы — отношениям между сущностями (моделями).
Модели в Rails могут иметь разные типы отношений: has_one (одном экземпляру принадлежит другой единственный экземпляр), has_many (одному экземпляру принадлежит несколько других экземпляров), belongs_to (экземпляр принадлежит другому экземпляру) и has_and_belongs_to_many. Для полного понимания того, что такое отношения (ассоциации), я кратко объясню что это на примере нашего проекта. Каждый пост (статья) в разрабатываемом блоге не может существовать просто так, она должна иметь пользователя — автора, другими словами пост принадлежит (belongs_to) пользователю. В это же время пользователь владеет постом, причем может владеть не одним, а некоторым количеством постов, то есть пользователь has_many :posts. Еще пользователь может иметь, например один номер телефона. Когда одной сущности может принадлежать только один экземпляр другой, то такое отношение описывается через has_one. Для того, чтобы установить отношения между моделями в нашем приложении, мы должны прописать в каждой модели эти отношения:
#post.rb модель class Post < ActiveRecord::Base belongs_to :user end #user.rb модель class User < ActiveRecord::Base has_many :posts end
Давайте откроем консоль и поиграемся с нашими моделями, чтобы лучше узнать как работают отношения.
$ rails c user = User.new do |u| u.name = 'Vasya' u.lname = 'Petrov' u.login = 'vaspet' u.password = 'pass123' u.email = 'vasya@petrov.ru' end #=> #<User id: nil, name: "Vasya", lname: "Petrov", login: "vaspet", password: "pass123", email: "vasya@petrov.ru", created_at: nil, updated_at: nil> user.save #=> true user.posts #=> [] post = Post.new do |p| p.title = "First post" p.content = "First blog post. First blog bost..." end #=> #<Post id: nil, title: "First post", content: "First blog post. First blog bost...", user_id: nil, created_at: nil, updated_at: nil> user.posts << post #=> [#<Post id: 1, title: "First post", content: "First blog post. First blog bost...", user_id: 1, created_at: "2011-04-13 17:20:48", updated_at: "2011-04-13 17:20:48">] user.save #=> true user.posts #=> [#<Post id: 1, title: "First post", content: "First blog post. First blog bost...", user_id: 1, created_at: "2011-04-13 17:20:48", updated_at: "2011-04-13 17:20:48">] post.user #=> #<User id: 1, name: "Vasya", lname: "Petrov", login: "vaspet", password: "pass123", email: "vasya@petrov.ru", created_at: "2011-04-13 17:16:55", updated_at: "2011-04-13 17:16:55">
Поскольку пост не может существовать сам по себе, то есть не может существовать без автора, то вам следует указать значение зависимости постов от пользователя в значение :destroy:
class User < ActiveRecord::Base has_many :posts, :dependent =>:destroy end
Теперь, при удалении пользователя вместе с ним будут удалены все его посты.
Также вам следует заняться валидацией ваших моделей. Валидация — это процесс проверки данных записанных в экземпляр модели перед его сохранением в базу данных. В результате валидации вы можете множество проверок, которые бы обеспечивали: полноту данных, качество данных, безопасность. В нашем примере нам необходимо проверить указан ли у поста его автор, если автор не указан, то пост не будет сохранен. Кроме проверки наличия автора у поста мы позаботимся о проверке других атрибутов экземпляров обеих моделей:
#models/user.rb class User < ActiveRecord::Base validates :name, :presence => true validates :lname, :presence => true validates :password, :presence => true validates :login, :presence => true, :uniqueness => true validates :email, :presence => true, :uniqueness => true has_many :posts, :dependent =>:destroy end #models/post.rb class Post < ActiveRecord::Base validates :user_id, :presence => true validates :title, :presence => true validates :content, :presence => true belongs_to :user end
Метод validates принимает в качестве аргументов имя проверяемого поля и тип проверки. :presence => true, означает что мы проверяем то, имеются ли в проверяемом поле какие-нибудь данные, а параметр :uniqueness => true, позволяет проверить уникальность данных введенных в поле. В нашем случае логин и адрес электронной почты должны не могут повторяться. Написание большого количества проверок считается хорошим тоном. Хотя время записи данных в базу увеличится, зато ваше приложение будет защищено от случайного или преднамеренного ввода неправильных данных, и будет требовать от пользования ввода всех необходимых для работы данных.
Только что мы закончили работу с постами, а значит, следует сделать коммит, слить ветку posts с веткой master и отправить изменения в удаленный репозиторий на GitHub:
$ git status
# On branch posts
# Changed but not updated:
# (use «git add <file>…» to update what will be committed)
# (use «git checkout — <file>…» to discard changes in working directory)
#
# modified: app/models/user.rb
# modified: config/routes.rb
# modified: db/schema.rb
#
# Untracked files:
# (use «git add <file>…» to include in what will be committed)
#
# app/controllers/posts_controller.rb
# app/helpers/posts_helper.rb
# app/models/post.rb
# app/views/posts/
# db/migrate/20110406172137_create_posts.rb
# spec/controllers/posts_controller_spec.rb
# spec/helpers/posts_helper_spec.rb
# spec/models/post_spec.rb
# spec/requests/posts_spec.rb
# spec/routing/posts_routing_spec.rb
# spec/views/posts/
no changes added to commit (use «git add» and/or «git commit -a»)
$ git commit -am ‘add posts’
[posts ef18d24] add posts
3 files changed, 18 insertions(+), 1 deletions(-)
$ git checkout master
Switched to branch ‘master’
$ git merge posts
Updating f29d0da..ef18d24
Fast-forward
app/models/user.rb | 7 +++++++
config/routes.rb | 2 ++
db/schema.rb | 10 +++++++++-
3 files changed, 18 insertions(+), 1 deletions(-)
$ git status
# On branch master
# Your branch is ahead of ‘origin/master’ by 1 commit.
#
# Untracked files:
# (use «git add <file>…» to include in what will be committed)
#
# app/controllers/posts_controller.rb
# app/helpers/posts_helper.rb
# app/models/post.rb
# app/views/posts/
# db/migrate/20110406172137_create_posts.rb
# spec/controllers/posts_controller_spec.rb
# spec/helpers/posts_helper_spec.rb
# spec/models/post_spec.rb
# spec/requests/posts_spec.rb
# spec/routing/posts_routing_spec.rb
# spec/views/posts/
nothing added to commit but untracked files present (use «git add» to track)
$ git commit -am ‘add posts’
[master 4af1c38] add posts
18 files changed, 427 insertions(+), 0 deletions(-)
create mode 100644 app/controllers/posts_controller.rb
create mode 100644 app/helpers/posts_helper.rb
create mode 100644 app/models/post.rb
create mode 100644 app/views/posts/_form.html.erb
create mode 100644 app/views/posts/edit.html.erb
create mode 100644 app/views/posts/index.html.erb
create mode 100644 app/views/posts/new.html.erb
create mode 100644 app/views/posts/show.html.erb
create mode 100644 db/migrate/20110406172137_create_posts.rb
create mode 100644 spec/controllers/posts_controller_spec.rb
create mode 100644 spec/helpers/posts_helper_spec.rb
create mode 100644 spec/models/post_spec.rb
create mode 100644 spec/requests/posts_spec.rb
create mode 100644 spec/routing/posts_routing_spec.rb
create mode 100644 spec/views/posts/edit.html.erb_spec.rb
create mode 100644 spec/views/posts/index.html.erb_spec.rb
create mode 100644 spec/views/posts/new.html.erb_spec.rb
create mode 100644 spec/views/posts/show.html.erb_spec.rb
$ git push origin
Counting objects: 64, done.
Compressing objects: 100% (43/43), done.
Writing objects: 100% (45/45), 6.76 KiB, done.
Total 45 (delta 7), reused 0 (delta 0)
To git@github.com:egoholic/blog.git
f29d0da..4af1c38 master -> master
Кроме модели User и Post, наш блог должен иметь модель Category, экземпляры которой представляют собой категории постов. В нашем блоге пост может принадлежать сразу нескольким категориям, а категория может иметь сразу несколько постов. Такие отношения называются отношения многие ко многим.
Давайте создадим модель Category и затем опишем отношения между ею и моделью Post. Для этого удалим ветку проекта posts и создадим новую ветку categories:
$ git branch -d posts
Deleted branch posts (was ef18d24).
$ git checkout -b categories
Switched to a new branch ‘categories’
Каждое новое более-менее значительное изменение в проекте следует выполнять в новой ветке и сейчас мы будем работать в ветке categories.
Создать отношение многие-ко многим можно в несколько способов, при помощи ассоциации has_many и параметра :through, и ассоциации has_and_belongs_to_many. Поскольку ассоциация has_and_belongs_to_many является моветоном и имеет несколько проблем, то использовать мы будем ассоциацию has_many :through. Используя ассоциацию has_and_belongs_to_many, вы создаете отношение многие ко многим напрямую при помощи дополнительной таблицы categories_posts. Когда же вы используете ассоциацию has_many :through, то отношение многие ко многим создается через дополнительную модель, которая также использует таблицу categories_posts, где хранятся записи состоящие из пар post_id и category_id, по которым можно произвести выборку всех записей с определенным category_id, а затем, по полю post_id этих записей сделать выборку постов из таблицы posts.
Для начала воспользуемся генератором scaffold для генерации модели Category и контроллера CategoriesController:
$ rails g scaffold Category name:string count:integer
invoke active_record
create db/migrate/20110414133930_create_categories.rb
create app/models/category.rb
invoke rspec
…
identical public/stylesheets/scaffold.css
Теперь, нам необходимо создать промежуточную модель CategoryPost:
$ rails g model CategoryPost post_id:integer category_id:integer
invoke active_record
create db/migrate/20110414135111_create_category_posts.rb
create app/models/category_post.rb
invoke rspec
create spec/models/category_post_spec.rb
Теперь выполним миграции, которые были созданы генераторами:
$ rake db:migrate
(in /home/vladimir/proj/blog)
== CreateCategories: migrating ===============================================
— create_table(:categories)
-> 0.0183s
== CreateCategories: migrated (0.0184s) ======================================
== CreateCategoryPosts: migrating ============================================
— create_table(:category_posts)
-> 0.0049s
== CreateCategoryPosts: migrated (0.0050s) ===================================
После того, как таблицы и модели созданы, необходимо заняться описанием отношений между таблицами:
#models/category_post.rb class CategoryPost < ActiveRecord::Base belongs_to :post belongs_to :category end #models/post.rb class Post < ActiveRecord::Base validates :user_id, :presence => true validates :title, :presence => true validates :content, :presence => true belongs_to :user has_many :category_posts, :dependent => :destroy has_many :categories, :through => :category_posts end #models/category.rb class Category < ActiveRecord::Base has_many :category_posts has_many :posts, :through => :category_posts end
Давайте запустим консоль и подробно рассмотрим как устроены отношения между моделями:
user = User.new do |u| u.name = 'Vladimir' u.lname = 'Melnik' u.login = 'admin' u.password = '12345' u.email = 'egotraumatic@gmail.com' end #=> #<User id: nil, name: "Vladimir", lname: "Melnik", login: "admin", password: "12345", email: "egotraumatic@gmail.com", created_at: nil, updated_at: nil> user.save #=> true category = Category.new(name: 'Ruby') #=> #<Category id: nil, name: "Ruby", count: nil, created_at: nil, updated_at: nil> category.save #=> true category2 = Category.new(name: 'Rails') #=> #<Category id: nil, name: "Rails", count: nil, created_at: nil, updated_at: nil> category2.save #=> true post = Post.new(title: "First post", content: "First Vladimir's blog post.") #=> #<Post id: nil, title: "First post", content: "First Vladimir's blog post.", user_id: nil, created_at: nil, updated_at: nil> user.posts << post #=> [#<Post id: 3, title: "First post", content: "First Vladimir's blog post.", user_id: 2, created_at: "2011-04-14 14:25:43", updated_at: "2011-04-14 14:25:43">] user.save #=> true category.posts << post => [#<Post id: 3, title: "First post", content: "First Vladimir's blog post.", user_id: 2, created_at: "2011-04-14 14:25:43", updated_at: "2011-04-14 14:25:43">] category.save #=> true category2.posts << post #=> [#<Post id: 3, title: "First post", content: "First Vladimir's blog post.", user_id: 2, created_at: "2011-04-14 14:25:43", updated_at: "2011-04-14 14:25:43">] category2.save #=> true post2 = Post.new(title: 'Second post', content: 'Second Vladimir's blog post') user.posts << post2 #=> [#<Post id: 3, title: "First post", content: "First Vladimir's blog post.", user_id: 2, created_at: "2011-04-14 14:25:43", updated_at: "2011-04-14 14:25:43">, #<Post id: 2, title: "Second post", content: "Second Vladimir's blog post.", user_id: 2, created_at: "2011-04-14 14:14:13", updated_at: "2011-04-14 14:28:15">] category2.posts << post2 #=> [#<Post id: 3, title: "First post", content: "First Vladimir's blog post.", user_id: 2, created_at: "2011-04-14 14:25:43", updated_at: "2011-04-14 14:25:43">, #<Post id: 2, title: "Second post", content: "Second Vladimir's blog post.", user_id: 2, created_at: "2011-04-14 14:14:13", updated_at: "2011-04-14 14:28:15">] user.save #=> true category.save #=> true user.posts #=> [#<Post id: 3, title: "First post", content: "First Vladimir's blog post.", user_id: 2, created_at: "2011-04-14 14:25:43", updated_at: "2011-04-14 14:25:43">, #<Post id: 2, title: "Second post", content: "Second Vladimir's blog post.", user_id: 2, created_at: "2011-04-14 14:14:13", updated_at: "2011-04-14 14:28:15">] category.posts #=> [#<Post id: 3, title: "First post", content: "First Vladimir's blog post.", user_id: 2, created_at: "2011-04-14 14:25:43", updated_at: "2011-04-14 14:25:43">] category2.posts #=> [#<Post id: 3, title: "First post", content: "First Vladimir's blog post.", user_id: 2, created_at: "2011-04-14 14:25:43", updated_at: "2011-04-14 14:25:43">, #<Post id: 2, title: "Second post", content: "Second Vladimir's blog post.", user_id: 2, created_at: "2011-04-14 14:14:13", updated_at: "2011-04-14 14:28:15">] post.categories #=> [#<Category id: 3, name: "Ruby", count: nil, created_at: "2011-04-14 14:23:14", updated_at: "2011-04-14 14:23:14">, #<Category id: 4, name: "Rails", count: nil, created_at: "2011-04-14 14:23:31", updated_at: "2011-04-14 14:23:31">] post2.categories #=> [#<Category id: 4, name: "Rails", count: nil, created_at: "2011-04-14 14:23:31", updated_at: "2011-04-14 14:23:31">]
Чтобы получить все посты данной категории, необходимо просто вызвать метод #posts у экземпляра категории. Чтобы получить список всех категорий — необходимо вызвать метод #categories у экземпляра поста.
Поскольку, мы уже завершили работу над категориями, следует слить ветки вместе:
$ git commit -am ‘add categories’
[categories 24f1679] add categories
3 files changed, 19 insertions(+), 1 deletions(-)
$ git checkout master
Switched to branch ‘master’
$ git merge categories
Updating 4af1c38..24f1679
Fast-forward
app/models/post.rb | 2 ++
config/routes.rb | 2 ++
db/schema.rb | 16 +++++++++++++++-
3 files changed, 19 insertions(+), 1 deletions(-)
$ git commit -m ‘add categories’
[master 1e7e910] add categories
21 files changed, 475 insertions(+), 0 deletions(-)
create mode 100644 app/controllers/categories_controller.rb
create mode 100644 app/helpers/categories_helper.rb
…
create mode 100644 spec/views/categories/show.html.erb_spec.rb
Итак, мы уже имеем практически полнофункциональный набросок проекта. Для полноты функционала нам необходимо добавить возможность комментирования постов. Каждый пост может иметь множество комментариев, а каждый комментарий может принадлежать одному единственному посту. Это значит, что здесь следует использовать ассоциацию has_many. Комментарии в нашем приложении — блоге будут представлены моделью Comment. Следует также заметить, что комментарий должен принадлежать одному и только одному пользователю, то есть каждый комментарий имеет одинаковые отношения как с постом, так и с пользователем. Давайте создадим модель, контроллер, представления и прочие файлы для комментариев при помощи уже знакомого генератора scaffold. Мы не будем создавать новых веток, а будем работать в ветке master потому, что изменения, которые мы вносим являются достаточно мелкими и проблем с ними возникнуть не должно. В примерах выше новые ветки проекта создавались в целях напомнить вам о работе с системой контроля версий git.
$ rails g scaffold comment title:string comment:text user_id:integer post_id:integer
invoke active_record
create db/migrate/20110414151117_create_comments.rb
create app/models/comment.rb
invoke rspec
…
invoke stylesheets
identical public/stylesheets/scaffold.css
$ rake db:migrate
(in /home/vladimir/proj/blog)
== CreateComments: migrating =================================================
— create_table(:comments)
-> 0.0023s
== CreateComments: migrated (0.0024s) ========================================
А теперь давайте установим отношения между моделями:
#models/comment.rb class Comment < ActiveRecord::Base belongs_to :user belongs_to :post end #models/user.rb class User < ActiveRecord::Base validates :name, :presence => true validates :lname, :presence => true validates :password, :presence => true validates :login, :presence => true, :uniqueness => true validates :email, :presence => true, :uniqueness => true has_many :posts, :dependent => :destroy has_many :comments, :dependent => :destroy end #models/post.rb class Post < ActiveRecord::Base validates :user_id, :presence => true validates :title, :presence => true validates :content, :presence => true belongs_to :user has_many :category_posts, :dependent => :destroy has_many :categories, :through => :category_posts has_many :comments, :dependent => :destroy end
Модель Comment, как и большинство моделей должна быть снабжена валидацией для поверки того, вся ли информация переданная в экземпляр комментариия является корректной:
class Comment < ActiveRecord::Base validates :comment, :presence => true validates :user_id, :presence => true validates :post_id, :presence => true belongs_to :user belongs_to :post end
В моделе Comment мы только что добавили проверку на то, что у комментария есть автор, есть пост к которому он относится и есть текст самого комментария.
На этом глава заканчивается, а в следущей, если ничего не поменяется, мы рассмотрим создание многоуровневых (вложенных) комментариев и более подробно познакомимся с работой контроллеров и представлений.
апреля 14, 2011 at 21:00 (#)
Классно, но все-же жду статью про тестирование, Rspec book тяжеловато идет а найти статьи для чайников в стиле rails+rspec+cucumber+webrat+factory_girls+… нелегко :-)
апреля 15, 2011 at 08:28 (#)
классно, перевариваем
апреля 15, 2011 at 08:29 (#)
2 0x2e
вместо девченок лучше
апреля 15, 2011 at 09:29 (#)
[quote]
Поскольку ассоциация [color=blue]has_and_belongs_to_many[/color] является муветоном и имеет несколько проблем,…
[/quote]
С этого места очень хотелось бы пояснений: почему плохой тон и какие проблемы?
апреля 15, 2011 at 11:55 (#)
Круто, но не устаю повторять, что для новичков вы пишете непонятно
апреля 15, 2011 at 16:13 (#)
А почему использовать связь has_and_belongs_to_many плохо ? и какие могут быть проблеммы при использовании этой чудо асоциации ?
апреля 15, 2011 at 17:09 (#)
2 Igas
чем лучше?
апреля 16, 2011 at 15:23 (#)
Павел, сейчас пишутся черновые версии статей, в дальнейшем они будут поправлены, возможно изменится структура и т.д. Было бы хорошо, если бы вы задали вопросы касательно того, что вам не ясно, я бы на них ответил в комментариях, а позже обновил бы саму статью.
мая 4, 2011 at 10:21 (#)
У меня такая ситуация: имеется несколько моделей, для начала две form и father, прописаны связи, в контроллере
Проблема в том что в БД сохраняются пустые значения. Данные с формы поля Father_name не передаются.
мая 4, 2011 at 13:03 (#)
Elka, немного не понял вашу проблему. Если я провильно представил, то, что вы написали, то модель Form принадлежит модели Father и в форме заполняются поля для обеих моделей. Если это так, то вам следует сделать так:
Теперь ваша форма может сохранять сразу две модели, одна из которых (Father) — вложенная.
мая 25, 2011 at 12:20 (#)
Почему в методе has_many используется имя таблицы, а в методе belongs_to имя класса?
мая 25, 2011 at 13:22 (#)
Bighamster, на самом деле здесь идет привязка не к имени контроллера и таблицы в БД, а к именам сущностей и читабельности кода.
has_many :comments — означает, что данной сущности принадлежит множество других сущностей — комментариев. has_many :comments читается как обычное предложение: Имеет много КОММЕНТАРИЕВ (множественное число — несколько комментариев).
belongs_to :post — Принадлежит одному посту (единственное число — один пост).
декабря 26, 2011 at 09:01 (#)
Устойчиво ругается на вызов user.posts:
1.9.2p290 :003 > user.posts
ActiveRecord::StatementInvalid: Could not find table ‘posts’
[вырезано]
[sysadmin@sysadmin blog]$ rails —version
Rails 3.1.3
[sysadmin@sysadmin blog]$
Что неправильно?
декабря 26, 2011 at 09:18 (#)
vladiboc, судя по всему нужно пиграцию для постов сделать…
$ rake db:migrate
декабря 26, 2011 at 09:44 (#)
Спасибо, получилось
января 26, 2012 at 18:21 (#)
Отличный материал — спасибо!!!
зы: правильно пишется мОветон, а не мУветон. поправьте плз :)
января 27, 2012 at 13:36 (#)
anonymous, спасибо, поправил.