Ruby on Rails 的学习曲线还算是有一点陡的, 作为一个初学者, 建议先看一下Ruby的语法书, 再看一下Rails的入门教材和示例 , 但真正做项目的时候, 可能就要和各种各样Gems打交道了, 因为自己走了很多弯路, 所以想把一些笔记分享出来, 希望对新生有点帮助.
我准备用一个Blog的示例把Scaffolding MongoDB Devise Omniauth CanCan串起来.
MongoDB
1.1 Help
1.2. CRUD
1.3. Security
1.4. Port
准备工作
Git
Scaffold
4.1. 添加字段
4.2. 验证输入
4.3. 表关联
4.4. 引用关联
首先讲 MongoDB , Mongo 取自 humongo us, 意思是大得无比的.
在Ubuntu上安装MongoDB相当简单. 其网站上有详细资料 , 我仅copy一下几条命令:
1
2
3
4
5
6
$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv 7F0CEB10
$ sudo vi /etc/apt/sources.list.d/10gen.list
deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen
$ sudo apt-get update
$ sudo apt-get install mongodb-10gen
配置文件, 可以改变数据库存储目录等, 默认是 /var/lib/mongodb
1
$ sudo vi /etc/mongodb.conf
数据库启动停用重启命令和其它service一样:
1
$ sudo service mongodb start | stop | restart
命令行输入mongo
可以打开数据库操作台:
1
2
3
4
> help // --top level help
> db . help () // --help on db-specific methods
> db . mycollection . help () // --help on collection methods
> db . mycollection . find (). help () // --cursor help
1
2
3
> show dbs // --displays all the databases on the server you are connected to
> use db_name // --switches to db_name on the same server
> show collections // --displays a list of all the collections in the current database
不能免俗, 可以在默认的test
数据库来个hello world!
:
1
2
3
> db . test . save ( { mongo : "Hello World!" } )
> db . test . find ()
{ "_id" : ObjectId ( "4fb947a9e7ffc7b413ce9c54" ), "mongo" : "Hello World!" }
C: create
1
2
> db . test . save ( { website : "blog.neten.de" } );
> db . test . save ( { website : "bbs.neten.de" } );
R: read
1
2
3
4
5
6
7
8
9
10
> db . test . find ();
{ "_id" : ObjectId ( "4fb947a9e7ffc7b413ce9c54" ), "mongo" : "Hello World!" }
{ "_id" : ObjectId ( "4fb94971e7ffc7b413ce9c55" ), "website" : "blog.neten.de" }
{ "_id" : ObjectId ( "4fb94978e7ffc7b413ce9c56" ), "website" : "bbs.neten.de" }
> db . test . findOne ( { mongo : "Hello World!" } );
{
"_id" : ObjectId ( "4fb947a9e7ffc7b413ce9c54" ),
"mongo" : "Hello World!" ,
"website" : "www.neten.de"
}
U: update
1
2
3
4
5
6
7
> person = db . test . findOne ( { mongo : "Hello World!" } );
> person . website = "www.neten.de" ;
> db . test . save ( person );
> db . test . find ();
{ "_id" : ObjectId ( "4fb94971e7ffc7b413ce9c55" ), "website" : "blog.neten.de" }
{ "_id" : ObjectId ( "4fb94978e7ffc7b413ce9c56" ), "website" : "bbs.neten.de" }
{ "_id" : ObjectId ( "4fb947a9e7ffc7b413ce9c54" ), "mongo" : "Hello World!" , "website" : "www.neten.de" }
D: delete
1
2
3
4
5
> db . test . drop () // --drop the entire test collection
> db . test . remove () // --remove all objects from the collection
> db . test . remove ( { mongo : "Hello World!" } ) // --remove objects from the collection where name is mongo
> use [ database ];
> db . dropDatabase ();
如果删除Collection之后, 可能要重启才能看到Collection不见了.
还有一个重要工作就是安全工作 :
添加管理员用户laoda(老大), 管理员的名字最好不要用admin, root之类的
1
2
3
4
5
6
7
8
9
10
> use admin
switched to db admin
> db . addUser ( "laoda" , "neten" )
{ "n" : 0 , "connectionId" : 2 , "err" : null , "ok" : 1 }
{
"user" : "laoda" ,
"readOnly" : false ,
"pwd" : "3d075670621dfa6f25d9b6b9caa1d987" ,
"_id" : ObjectId ( "4fbb5c2c124f3221f45162b4" )
}
验证函数:
1
2
3
4
> db . auth ( "laoda" , "neten" )
1
> db . auth ( "laoda" , "neten.de" )
0
添加普通用户xiaodi(小弟)和只读用户(龙套):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> use test
switched to db test
> db . addUser ( "xiaodi" , "neten" )
{ "n" : 0 , "connectionId" : 2 , "err" : null , "ok" : 1 }
{
"user" : "xiaodi" ,
"readOnly" : false ,
"pwd" : "8cb78b1a918c6c5cca2b9cea00673180" ,
"_id" : ObjectId ( "4fbb5c96124f3221f45162b5" )
}
> db . addUser ( "longtao" , "neten" , true )
{ "n" : 0 , "connectionId" : 2 , "err" : null , "ok" : 1 }
{
"user" : "longtao" ,
"readOnly" : true ,
"pwd" : "fe130c893dd5c69a5a1cb96feba00f7d" ,
"_id" : ObjectId ( "4fbb5cc7124f3221f45162b6" )
}
虽然我取的密码都是neten, 但hash过后的字符串都不一样, 比md5靠谱.
查找当前数据库用户:
1
2
3
4
> db . system . users . find ()
{ "_id" : ObjectId ( "4fbb5c96124f3221f45162b5" ), "user" : "xiaodi" , "readOnly" : false , "pwd" : "8cb78b1a918c6c5cca2b9cea00673180" }
{ "_id" : ObjectId ( "4fbb5cc7124f3221f45162b6" ), "user" : "longtao" , "readOnly" : true , "pwd" : "fe130c893dd5c69a5a1cb96feba00f7d" }
> db . removeUser ( username )
删除用户, 以下两种方法效果一样:
1
2
db . removeUser ( "longtao" )
db . system . users . remove ( { user : "longtao" } )
数据库默认端口:
1
2
3
4
Standalone mongod : 27017
mongos : 27017
shard server (mongod --shardsvr) : 27018
config server (mongod --configsvr) : 27019
第一步, 创建一个Rails App:
1
$ rails new neten -T -O
用-T -O 是为了不产生Test::Unit 和 Active Record 文件. 由rails产生的Gemfile修改后的内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
source 'https://rubygems.org'
gem 'rails' , '3.2.3'
group :assets do
gem 'sass-rails' , '~> 3.2.3'
gem 'coffee-rails' , '~> 3.2.1'
gem 'uglifier' , '>= 1.0.3'
end
gem 'jquery-rails' , '2.0.2'
gem "mongoid" , "~> 2.4"
gem "bson_ext" , "~> 1.5"
最后两行是mongoid相关的.
为项目管理方便, 可以创建gemset, 并设置它作为默认的gemset
1
2
3
$ rvm --create 1.9.3@neten
$ rvm --default use 1.9.3@neten
$ echo "rvm 1.9.3@neten" > .rvmrc
安装gems, 并检查本机安装的gems, 最好把Gemfile里面的gems都按gem list
所显示的标上版本号, 这样在远程部署的时候就不会出现版本问题.
1
2
$ bundle install
$ gem list --local
为了简化过程, 在此就不涉及RSpec了, 但无用的test模块可以删除.
1
2
3
4
5
$ rm -rf test /
$ vi config/application.rb
# ...
# require 'rails/test_unit/railtie'
# ...
使用Mongoid, 执行下面的命令后, 会生成配置文件config/mongoid.yml
1
$ rails generate mongoid:config
Mongoid会自动处理config/application.rb
文件, 禁用ActiveRecord, require 'rails/all'
会被以下代码替代:
1
2
3
require "action_controller/railtie"
require "action_mailer/railtie"
require "active_resource/railtie"
原来的数据库配置文件config/database.yml
可以删除了.
做了这么多工作, git是时候要登场了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ vi .gitignore
.bundle
db/*.sqlite3*
log/*.log
*.log
tmp/**/*
tmp/*
doc/api
doc/app
*.swp
*~
.*~
$ git init
Initialized empty Git repository in /home/user/neten/.git/
$ git add .
$ git commit -am "Initial commit"
用 scaffold 生成文章系统, 主要copy于railscasts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ rails g scaffold article name:string content:text
invoke mongoid
create app/models/article.rb
route resources :articles
invoke scaffold_controller
create app/controllers/articles_controller.rb
invoke erb
create app/views/articles
create app/views/articles/index.html.erb
create app/views/articles/edit.html.erb
create app/views/articles/show.html.erb
create app/views/articles/new.html.erb
create app/views/articles/_form.html.erb
invoke helper
create app/helpers/articles_helper.rb
invoke assets
invoke coffee
create app/assets/javascripts/articles.js.coffee
invoke scss
create app/assets/stylesheets/articles.css.scss
invoke scss
create app/assets/stylesheets/scaffolds.css.scss
事就这么成了:) rake db:migrate
? 不用了, MongoDB是schemaless database.
因为首页还是默认的Welcome页面, 所以要处理一下:
1
2
3
4
5
6
$ rm public/index.html
$ vi config/routes.rb
Neten::Application.routes.draw do
resources :articles
root :to = > 'articles#index'
end
现在去http://localhost:3000 预览一下吧
Mongoid为model提供了generator, ActiveRecord
没有用到, 包含了Mongoid::Document
,如果到现在才发现没有加入文章发布时间也关系不大, MongoDB嘛, schema-less, 好处就是在model直接加个字段名称就好了, 不用在db文件夹那里添加东西了.
另外因为加入了Date
类型的字段, 所以要加上这句include Mongoid::MultiParameterAttributes
, 不然没办法显示出来 .
/app/models/article.rb 1
2
3
4
5
6
7
class Article
include Mongoid :: Document
include Mongoid :: MultiParameterAttributes
field :name , :type => String
field :content , :type => String
field :published_on , :type => Date
end
view也要相应改一下:
/app/views/articles/_form.html.erb 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="field">
<%= f . label :name %> <br />
<%= f . text_field :name %>
</div>
<div class="field">
<%= f . label :published_on %> <br />
<%= f . date_select :published_on %>
</div>
<div class="field">
<%= f . label :content %> <br />
<%= f . text_area :content %>
</div>
<div class="actions">
<%= f . submit %>
</div>
/app/views/articles/show.html.erb 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<p id="notice"> <%= notice %> </p>
<p>
<b>Name:</b>
<%= @article . name %>
</p>
<p>
<b>Content:</b>
<%= @article . content %>
</p>
<p>
<b>Published:</b>
<%= @article . published_on %>
</p>
<%= link_to 'Edit' , edit_article_path ( @article ) %> |
<%= link_to 'Back' , articles_path %>
<%= debug @article %>
<%= debug @article %>
是调试信息, 有这句话, 在前台页面上就会在页脚出现@article的信息.
Mongoid 会使用 ActiveModel 来处理一些事务, 这就有点像我们熟悉的ActiveRecord一样. 比如说在ActiveRecord中用到的validations, callbacks, dirty tracking, attr_accessible都可以搬过来用.
下面我们为:name
字段加上 必需输入 的验证要求:
/app/models/article.rb 1
2
3
4
5
6
7
8
class Article
include Mongoid :: Document
include Mongoid :: MultiParameterAttributes
field :name , :type => String
field :content , :type => String
field :published_on , :type => Date
validates_presence_of :name
end
不允许评论的blog会表现得有点言论暴力, 要加上comments
在用数据库的情况下, 一个has_many
, 就解决问题了, 现在我们就要在mongoid中寻找has_many
的替代了.
/app/models/article.rb 1
2
3
4
5
6
7
8
9
class Article
include Mongoid :: Document
include Mongoid :: MultiParameterAttributes
field :name , :type => String
field :content , :type => String
field :published_on , :type => Date
validates_presence_of :name
embeds_many :comments
end
添加完关联后, 来生成评论model
1
2
3
$ rails g model comment name:string content:text
invoke mongoid
create app/models/comment.rb
再让comment和article手拉手:
app/models/comment.rb 1
2
3
4
5
6
class Comment
include Mongoid :: Document
field :name
field :content
embedded_in :article , :inverse_of => :comments
end
embedded_in
顾名思意就是说Comment是嵌在Article里面的, inverse_of
呢是表明comment是通过comments嵌套在Article里面的.
这个手拉手的关系也要在routes里面申明一下:
confing/routes.rb 1
2
3
4
5
Neten :: Application . routes . draw do
resources :articles do
resources :comments
end
end
好了, 现在创建comment的controller
1
2
3
4
5
6
7
8
9
10
11
$ rails g controller comments
create app/controllers/comments_controller.rb
invoke erb
create app/views/comments
invoke helper
create app/helpers/comments_helper.rb
invoke assets
invoke coffee
create app/assets/javascripts/comments.js.coffee
invoke scss
create app/assets/stylesheets/comments.css.scss
Comment和Article手拉手之后, 一切都要看Article的眼色(:article_id)行事
app/controllers/comments_controller.rb 1
2
3
4
5
6
7
class CommentsController < ApplicationController
def create
@article = Article . find ( params [ :article_id ] )
@comment = @article . comments . create! ( params [ :comment ] )
redirect_to @article , :notice => "Comment created!"
end
end
在app/views/articles/show.html.erb
的最后面加上comments的代码:
app/views/articles/show.html.erb 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<% if @article . comments . size > 0 %>
<h2>Comments</h2>
<% for comment in @article . comments %>
<h3> <%= comment . name %> </h3>
<p> <%= comment . content %> </p>
<% end %>
<% end %>
<h2>New Comment</h2>
<%= form_for [ @article , Comment . new ] do | f | %>
<p> <%= f . label :name %> <%= f . text_field :name %> </p>
<p> <%= f . text_area :content , :rows => 10 %> </p>
<p> <%= f . submit %> </p>
<% end %>
好, 现在打开网站尽情地发表自己的观点吧.
有的blog可能是夫妻店, 所以把Blog的作者放到文章里面还是很有用处的, scaffold又要上班了.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ rails g scaffold author name:string
invoke mongoid
create app/models/author.rb
route resources :authors
invoke scaffold_controller
create app/controllers/authors_controller.rb
invoke erb
create app/views/authors
create app/views/authors/index.html.erb
create app/views/authors/edit.html.erb
create app/views/authors/show.html.erb
create app/views/authors/new.html.erb
create app/views/authors/_form.html.erb
invoke helper
create app/helpers/authors_helper.rb
invoke assets
invoke coffee
create app/assets/javascripts/authors.js.coffee
invoke scss
create app/assets/stylesheets/authors.css.scss
invoke scss
identical app/assets/stylesheets/scaffolds.css.scss
添加一个key字段, 这样可以在生成链接的时候, 将小写的:name字段做链接名称, 并在表中作_id. 中文没有小写, 不过没关系, 直接用汉字做链接, 比如: http://localhost:3000/authors/张三/edit
, references_many
可以定义其与articles的关系.
app/models/author.rb 1
2
3
4
5
6
class Author
include Mongoid :: Document
field :name , :type => String
key :name , :type => String
references_many :articles
end
用这个链接可以添加一些作者 http://localhost:3000/authors/
添加过后去命令行看一下:
1
2
3
4
5
> use neten_development
switched to db neten_development
> db . authors . find ()
{ "_id" : "peter" , "name" : "Peter" }
{ "_id" : "paul" , "name" : "Paul" }
在app/models/article.rb
里面要用referenced_in
.
app/models/article.rb 1
2
3
4
5
6
7
8
9
class Article
include Mongoid :: Document
field :name
field :content
field :published_on , :type => Date
validates_presence_of :name
embeds_many :comments
referenced_in :author
end
为了方便写Blog的人能够添加作者, 我们可以建立一个Select代码.
app/views/articles/_form.html.erb 1
2
3
4
<div class="field">
<%= f . label :author_id %> <br />
<%= f . collection_select :author_id , Author . all , :id , :name %>
</div>
当然不要忘记在展示页面也要让作者出现, 因为前面有可能没有定义作者, 还是加个if吧, 这样不会出错.
app/views/articles/show.html.erb 1
2
3
4
5
6
<% if @article . author . name . size > 0 %>
<p>
<b>Author:</b>
<%= @article . author . name %>
</p>
<% end %>
最后总结一下Comment和Author, 因为Comments是Embeded在Articles中的, 所以它就是个附庸, 没有自己的家(Collection), 而Author是一个Reference, 所以它有自己的房子(Collection).
1
2
3
4
> show collections
articles
authors
system . indexes
从Rails控制台检索一下结果:
1
2
3
4
5
6
7
$ rails c
Loading development environment ( Rails 3 . 2 . 3 )
1 . 9 . 3 p194 : 001 > Article . first
=> #<Article _id: 4fbd001de6fc8c2470000002, _type: nil, name: "first", content: "first post", published_on: 2012-03-03 00:00:00 UTC, author_id: "peter", published_on(1i): "2012", published_on(2i): "3", published_on(3i): "3">
1 . 9 . 3 p194 : 00 9 > Article . first . comments
=> [ #<Comment _id: 4fbe5755e6fc8c6c3f000004, _type: nil, name: "good", content: "this is a awesome blog">]
从MongoDB的命令行检索一下, 和上面作个对比:
1
2
3
4
5
$ mongo
> use neten_development
switched to db neten_development
> db . articles . find ( { name : "first" } )
{ "_id" : ObjectId ( "4fbd001de6fc8c2470000002" ), "author_id" : "peter" , "comments" : [ { "_id" : ObjectId ( "4fbe5755e6fc8c6c3f000004" ), "name" : "good" , "content" : "this is a awesome blog" } ], "content" : "first post" , "name" : "first" , "published_on" : ISODate ( "2012-03-03T00:00:00Z" ), "published_on(1i)" : "2012" , "published_on(2i)" : "3" , "published_on(3i)" : "3" }