简洁的想法

仁爱、喜乐、和平、忍耐、恩慈、良善、信实、温柔、节制

MongoDB Scaffolding Devise Omniauth CanCan (1)

| Comments

Ruby on Rails 的学习曲线还算是有一点陡的, 作为一个初学者, 建议先看一下Ruby的语法书, 再看一下Rails的入门教材和示例, 但真正做项目的时候, 可能就要和各种各样Gems打交道了, 因为自己走了很多弯路, 所以想把一些笔记分享出来, 希望对新生有点帮助.

我准备用一个Blog的示例把Scaffolding MongoDB Devise Omniauth CanCan串起来.

  1. MongoDB
    1.1 Help
    1.2. CRUD
    1.3. Security
    1.4. Port
  2. 准备工作
  3. Git
  4. Scaffold
    4.1. 添加字段
    4.2. 验证输入
    4.3. 表关联
    4.4. 引用关联

MongoDB

首先讲 MongoDB, Mongo 取自 humongous, 意思是大得无比的.

在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可以打开数据库操作台:

Help

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!" }

CRUD

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不见了.

Security

还有一个重要工作就是安全工作:

添加管理员用户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"})

Port

数据库默认端口:

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

做了这么多工作, 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

用 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的信息.

验证输入 Validations

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

表关联 Associations

不允许评论的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 %>

好, 现在打开网站尽情地发表自己的观点吧.

引用关联 Reference Type Associations

有的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.3p194 :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.3p194 :009 > 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" }

Comments